]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
Rearchitect the Shell to have a components system
authorJasper St. Pierre <jstpierre@mecheye.net>
Mon, 3 Sep 2012 01:23:50 +0000 (22:23 -0300)
committerJasper St. Pierre <jstpierre@mecheye.net>
Tue, 4 Sep 2012 21:42:44 +0000 (18:42 -0300)
Components are pieces of the shell code that can be added/removed
at runtime, like extension, but are tied more directly to a session
mode. The session polkit agent, the network agent, autorun/automount,
are all components, keyring, recorder and telepathy client are all
now copmonents.

https://bugzilla.gnome.org/show_bug.cgi?id=683156

19 files changed:
js/Makefile.am
js/ui/automountManager.js [deleted file]
js/ui/autorunManager.js [deleted file]
js/ui/components/__init__.js [new file with mode: 0644]
js/ui/components/automountManager.js [new file with mode: 0644]
js/ui/components/autorunManager.js [new file with mode: 0644]
js/ui/components/keyring.js [new file with mode: 0644]
js/ui/components/networkAgent.js [new file with mode: 0644]
js/ui/components/polkitAgent.js [new file with mode: 0644]
js/ui/components/recorder.js [new file with mode: 0644]
js/ui/components/telepathyClient.js [new file with mode: 0644]
js/ui/keyringPrompt.js [deleted file]
js/ui/main.js
js/ui/networkAgent.js [deleted file]
js/ui/polkitAuthenticationAgent.js [deleted file]
js/ui/sessionMode.js
js/ui/telepathyClient.js [deleted file]
src/shell-polkit-authentication-agent.c
src/shell-polkit-authentication-agent.h

index 330098105664499e39aed952279575b83555c91d..351ced1f5119ff548dd45e5c3a852c7753249c5e 100644 (file)
@@ -1,3 +1,4 @@
+NULL =
 
 EXTRA_DIST = misc/config.js.in
 CLEANFILES = misc/config.js
@@ -37,8 +38,6 @@ nobase_dist_js_DATA =         \
        ui/altTab.js            \
        ui/appDisplay.js        \
        ui/appFavorites.js      \
-       ui/automountManager.js  \
-       ui/autorunManager.js    \
        ui/boxpointer.js        \
        ui/calendar.js          \
        ui/checkBox.js          \
@@ -47,15 +46,14 @@ nobase_dist_js_DATA =       \
        ui/dateMenu.js          \
        ui/dnd.js               \
        ui/endSessionDialog.js  \
-       ui/environment.js       \
        ui/extensionSystem.js   \
        ui/extensionDownloader.js \
+       ui/environment.js       \
        ui/flashspot.js         \
        ui/ibusCandidatePopup.js\
        ui/grabHelper.js        \
        ui/iconGrid.js          \
        ui/keyboard.js          \
-       ui/keyringPrompt.js     \
        ui/layout.js            \
        ui/lightbox.js          \
        ui/lookingGlass.js      \
@@ -64,7 +62,6 @@ nobase_dist_js_DATA =         \
        ui/main.js              \
        ui/messageTray.js       \
        ui/modalDialog.js       \
-       ui/networkAgent.js      \
        ui/sessionMode.js       \
        ui/shellEntry.js        \
        ui/shellMountOperation.js \
@@ -74,7 +71,6 @@ nobase_dist_js_DATA =         \
        ui/panelMenu.js         \
        ui/placeDisplay.js      \
        ui/pointerWatcher.js    \
-       ui/polkitAuthenticationAgent.js \
        ui/popupMenu.js         \
        ui/remoteSearch.js      \
        ui/runDialog.js         \
@@ -90,7 +86,6 @@ nobase_dist_js_DATA =         \
        ui/status/power.js      \
        ui/status/volume.js     \
        ui/status/bluetooth.js  \
-       ui/telepathyClient.js   \
        ui/tweener.js           \
        ui/unlockDialog.js      \
        ui/userMenu.js          \
@@ -102,4 +97,13 @@ nobase_dist_js_DATA =       \
        ui/workspaceThumbnail.js        \
        ui/workspacesView.js    \
        ui/workspaceSwitcherPopup.js    \
-       ui/xdndHandler.js
+       ui/xdndHandler.js       \
+       ui/components/__init__.js               \
+       ui/components/autorunManager.js         \
+       ui/components/automountManager.js       \
+       ui/components/networkAgent.js           \
+       ui/components/polkitAgent.js            \
+       ui/components/recorder.js               \
+       ui/components/telepathyClient.js        \
+       ui/components/keyring.js                \
+       $(NULL)
diff --git a/js/ui/automountManager.js b/js/ui/automountManager.js
deleted file mode 100644 (file)
index a49eb17..0000000
+++ /dev/null
@@ -1,261 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const Lang = imports.lang;
-const Mainloop = imports.mainloop;
-const GLib = imports.gi.GLib;
-const Gio = imports.gi.Gio;
-const Params = imports.misc.params;
-const Shell = imports.gi.Shell;
-
-const GnomeSession = imports.misc.gnomeSession;
-const LoginManager = imports.misc.loginManager;
-const Main = imports.ui.main;
-const ShellMountOperation = imports.ui.shellMountOperation;
-
-const GNOME_SESSION_AUTOMOUNT_INHIBIT = 16;
-
-// GSettings keys
-const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
-const SETTING_ENABLE_AUTOMOUNT = 'automount';
-
-const AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
-
-const AutomountManager = new Lang.Class({
-    Name: 'AutomountManager',
-
-    _init: function() {
-        this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
-        this._volumeQueue = [];
-        this._session = new GnomeSession.SessionManager();
-        this._session.connectSignal('InhibitorAdded',
-                                    Lang.bind(this, this._InhibitorsChanged));
-        this._session.connectSignal('InhibitorRemoved',
-                                    Lang.bind(this, this._InhibitorsChanged));
-        this._inhibited = false;
-
-        this._loginManager = LoginManager.getLoginManager();
-
-        Main.screenShield.connect('lock-status-changed', Lang.bind(this, this._lockStatusChanged));
-
-        this._volumeMonitor = Gio.VolumeMonitor.get();
-
-        this._volumeMonitor.connect('volume-added',
-                                    Lang.bind(this,
-                                              this._onVolumeAdded));
-        this._volumeMonitor.connect('volume-removed',
-                                    Lang.bind(this,
-                                              this._onVolumeRemoved));
-        this._volumeMonitor.connect('drive-connected',
-                                    Lang.bind(this,
-                                              this._onDriveConnected));
-        this._volumeMonitor.connect('drive-disconnected',
-                                    Lang.bind(this,
-                                              this._onDriveDisconnected));
-        this._volumeMonitor.connect('drive-eject-button',
-                                    Lang.bind(this,
-                                              this._onDriveEjectButton));
-
-        Mainloop.idle_add(Lang.bind(this, this._startupMountAll));
-    },
-
-    _InhibitorsChanged: function(object, senderName, [inhibtor]) {
-        this._session.IsInhibitedRemote(GNOME_SESSION_AUTOMOUNT_INHIBIT,
-            Lang.bind(this,
-                function(result, error) {
-                    if (!error) {
-                        this._inhibited = result[0];
-                    }
-                }));
-    },
-
-    _lockStatusChanged: function(shield, locked) {
-        if (!locked) {
-            this._volumeQueue.forEach(Lang.bind(this, function(volume) {
-                this._checkAndMountVolume(volume);
-            }));
-        }
-
-        // clear the queue anyway
-        this._volumeQueue = [];
-    },
-
-    _startupMountAll: function() {
-        let volumes = this._volumeMonitor.get_volumes();
-        volumes.forEach(Lang.bind(this, function(volume) {
-            this._checkAndMountVolume(volume, { checkSession: false,
-                                                useMountOp: false,
-                                                allowAutorun: false });
-        }));
-
-        return false;
-    },
-
-    _onDriveConnected: function() {
-        // if we're not in the current ConsoleKit session,
-        // or screensaver is active, don't play sounds
-        if (!this._loginManager.sessionActive)
-            return;
-
-        if (Main.screenShield.locked)
-            return;
-
-        global.play_theme_sound(0, 'device-added-media');
-    },
-
-    _onDriveDisconnected: function() {
-        // if we're not in the current ConsoleKit session,
-        // or screensaver is active, don't play sounds
-        if (!this._loginManager.sessionActive)
-            return;
-
-        if (Main.screenShield.locked)
-            return;
-
-        global.play_theme_sound(0, 'device-removed-media');        
-    },
-
-    _onDriveEjectButton: function(monitor, drive) {
-        // TODO: this code path is not tested, as the GVfs volume monitor
-        // doesn't emit this signal just yet.
-        if (!this._loginManager.sessionActive)
-            return;
-
-        // we force stop/eject in this case, so we don't have to pass a
-        // mount operation object
-        if (drive.can_stop()) {
-            drive.stop
-                (Gio.MountUnmountFlags.FORCE, null, null,
-                 Lang.bind(this, function(drive, res) {
-                     try {
-                         drive.stop_finish(res);
-                     } catch (e) {
-                         log("Unable to stop the drive after drive-eject-button " + e.toString());
-                     }
-                 }));
-        } else if (drive.can_eject()) {
-            drive.eject_with_operation 
-                (Gio.MountUnmountFlags.FORCE, null, null,
-                 Lang.bind(this, function(drive, res) {
-                     try {
-                         drive.eject_with_operation_finish(res);
-                     } catch (e) {
-                         log("Unable to eject the drive after drive-eject-button " + e.toString());
-                     }
-                 }));
-        }
-    },
-
-    _onVolumeAdded: function(monitor, volume) {
-        this._checkAndMountVolume(volume);
-    },
-
-    _checkAndMountVolume: function(volume, params) {
-        params = Params.parse(params, { checkSession: true,
-                                        useMountOp: true,
-                                        allowAutorun: true });
-
-        if (params.checkSession) {
-            // if we're not in the current ConsoleKit session,
-            // don't attempt automount
-            if (!this._loginManager.sessionActive)
-                return;
-
-            if (Main.screenShield.locked) {
-                if (this._volumeQueue.indexOf(volume) == -1)
-                    this._volumeQueue.push(volume);
-
-                return;
-            }
-        }
-
-        if (this._inhibited)
-            return;
-
-        // Volume is already mounted, don't bother.
-        if (volume.get_mount())
-            return;
-
-        if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) ||
-            !volume.should_automount() ||
-            !volume.can_mount()) {
-            // allow the autorun to run anyway; this can happen if the
-            // mount gets added programmatically later, even if 
-            // should_automount() or can_mount() are false, like for
-            // blank optical media.
-            this._allowAutorun(volume);
-            this._allowAutorunExpire(volume);
-
-            return;
-        }
-
-        if (params.useMountOp) {
-            let operation = new ShellMountOperation.ShellMountOperation(volume);
-            this._mountVolume(volume, operation, params.allowAutorun);
-        } else {
-            this._mountVolume(volume, null, params.allowAutorun);
-        }
-    },
-
-    _mountVolume: function(volume, operation, allowAutorun) {
-        if (allowAutorun)
-            this._allowAutorun(volume);
-
-        let mountOp = operation ? operation.mountOp : null;
-        volume._operation = operation;
-
-        volume.mount(0, mountOp, null,
-                     Lang.bind(this, this._onVolumeMounted));
-    },
-
-    _onVolumeMounted: function(volume, res) {
-        this._allowAutorunExpire(volume);
-
-        try {
-            volume.mount_finish(res);
-            this._closeOperation(volume);
-        } catch (e) {
-            // FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks
-            // backend in this case, see 
-            // https://bugs.freedesktop.org/show_bug.cgi?id=51271
-            if (e.message.indexOf('No key available with this passphrase') != -1) {
-                this._reaskPassword(volume);
-            } else {
-                if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
-                    log('Unable to mount volume ' + volume.get_name() + ': ' + e.toString());
-
-                this._closeOperation(volume);
-            }
-        }
-    },
-
-    _onVolumeRemoved: function(monitor, volume) {
-        this._volumeQueue = 
-            this._volumeQueue.filter(function(element) {
-                return (element != volume);
-            });
-    },
-
-    _reaskPassword: function(volume) {
-        let existingDialog = volume._operation ? volume._operation.borrowDialog() : null;
-        let operation = 
-            new ShellMountOperation.ShellMountOperation(volume,
-                                                        { existingDialog: existingDialog });
-        this._mountVolume(volume, operation);
-    },
-
-    _closeOperation: function(volume) {
-        if (volume._operation)
-            volume._operation.close();
-    },
-
-    _allowAutorun: function(volume) {
-        volume.allowAutorun = true;
-    },
-
-    _allowAutorunExpire: function(volume) {
-        Mainloop.timeout_add_seconds(AUTORUN_EXPIRE_TIMEOUT_SECS, function() {
-            volume.allowAutorun = false;
-            return false;
-        });
-    }
-});
diff --git a/js/ui/autorunManager.js b/js/ui/autorunManager.js
deleted file mode 100644 (file)
index 2776fbb..0000000
+++ /dev/null
@@ -1,595 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const Lang = imports.lang;
-const Gio = imports.gi.Gio;
-const St = imports.gi.St;
-
-const LoginManager = imports.misc.loginManager;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const ShellMountOperation = imports.ui.shellMountOperation;
-
-// GSettings keys
-const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
-const SETTING_DISABLE_AUTORUN = 'autorun-never';
-const SETTING_START_APP = 'autorun-x-content-start-app';
-const SETTING_IGNORE = 'autorun-x-content-ignore';
-const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
-
-const AutorunSetting = {
-    RUN: 0,
-    IGNORE: 1,
-    FILES: 2,
-    ASK: 3
-};
-
-// misc utils
-function shouldAutorunMount(mount, forTransient) {
-    let root = mount.get_root();
-    let volume = mount.get_volume();
-
-    if (!volume || (!volume.allowAutorun && forTransient))
-        return false;
-
-    if (!root.is_native() || isMountRootHidden(root))
-        return false;
-
-    return true;
-}
-
-function isMountRootHidden(root) {
-    let path = root.get_path();
-
-    // skip any mounts in hidden directory hierarchies
-    return (path.indexOf('/.') != -1);
-}
-
-function startAppForMount(app, mount) {
-    let files = [];
-    let root = mount.get_root();
-    let retval = false;
-
-    files.push(root);
-
-    try {
-        retval = app.launch(files, 
-                            global.create_app_launch_context())
-    } catch (e) {
-        log('Unable to launch the application ' + app.get_name()
-            + ': ' + e.toString());
-    }
-
-    return retval;
-}
-
-/******************************************/
-
-const HotplugSnifferIface = <interface name="org.gnome.Shell.HotplugSniffer">
-<method name="SniffURI">
-    <arg type="s" direction="in" />
-    <arg type="as" direction="out" />
-</method>
-</interface>;
-
-const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(HotplugSnifferIface);
-function HotplugSniffer() {
-    return new HotplugSnifferProxy(Gio.DBus.session,
-                                   'org.gnome.Shell.HotplugSniffer',
-                                   '/org/gnome/Shell/HotplugSniffer');
-}
-
-const ContentTypeDiscoverer = new Lang.Class({
-    Name: 'ContentTypeDiscoverer',
-
-    _init: function(callback) {
-        this._callback = callback;
-    },
-
-    guessContentTypes: function(mount) {
-        // guess mount's content types using GIO
-        mount.guess_content_type(false, null,
-                                 Lang.bind(this,
-                                           this._onContentTypeGuessed));
-    },
-
-    _onContentTypeGuessed: function(mount, res) {
-        let contentTypes = [];
-
-        try {
-            contentTypes = mount.guess_content_type_finish(res);
-        } catch (e) {
-            log('Unable to guess content types on added mount ' + mount.get_name()
-                + ': ' + e.toString());
-        }
-
-        if (contentTypes.length) {
-            this._emitCallback(mount, contentTypes);
-        } else {
-            let root = mount.get_root();
-
-            let hotplugSniffer = new HotplugSniffer();
-            hotplugSniffer.SniffURIRemote(root.get_uri(),
-                 Lang.bind(this, function([contentTypes]) {
-                     this._emitCallback(mount, contentTypes);
-                 }));
-        }
-    },
-
-    _emitCallback: function(mount, contentTypes) {
-        if (!contentTypes)
-            contentTypes = [];
-
-        // we're not interested in win32 software content types here
-        contentTypes = contentTypes.filter(function(type) {
-            return (type != 'x-content/win32-software');
-        });
-
-        let apps = [];
-        contentTypes.forEach(function(type) {
-            let app = Gio.app_info_get_default_for_type(type, false);
-
-            if (app)
-                apps.push(app);
-        });
-
-        if (apps.length == 0)
-            apps.push(Gio.app_info_get_default_for_type('inode/directory', false));
-
-        this._callback(mount, apps, contentTypes);
-    }
-});
-
-const AutorunManager = new Lang.Class({
-    Name: 'AutorunManager',
-
-    _init: function() {
-        this._loginManager = LoginManager.getLoginManager();
-
-        this._volumeMonitor = Gio.VolumeMonitor.get();
-
-        this._volumeMonitor.connect('mount-added',
-                                    Lang.bind(this,
-                                              this._onMountAdded));
-        this._volumeMonitor.connect('mount-removed',
-                                    Lang.bind(this,
-                                              this._onMountRemoved));
-
-        this._transDispatcher = new AutorunTransientDispatcher();
-        this._createResidentSource();
-
-        let mounts = this._volumeMonitor.get_mounts();
-
-        mounts.forEach(Lang.bind(this, function (mount) {
-            let discoverer = new ContentTypeDiscoverer(Lang.bind (this, 
-                function (mount, apps) {
-                    this._residentSource.addMount(mount, apps);
-                }));
-
-            discoverer.guessContentTypes(mount);
-        }));
-    },
-
-    _createResidentSource: function() {
-        this._residentSource = new AutorunResidentSource();
-        this._residentSource.connect('destroy',
-                                     Lang.bind(this,
-                                               this._createResidentSource));
-    },
-
-    _onMountAdded: function(monitor, mount) {
-        // don't do anything if our session is not the currently
-        // active one
-        if (!this._loginManager.sessionActive)
-            return;
-
-        let discoverer = new ContentTypeDiscoverer(Lang.bind (this,
-            function (mount, apps, contentTypes) {
-                this._transDispatcher.addMount(mount, apps, contentTypes);
-                this._residentSource.addMount(mount, apps);
-            }));
-
-        discoverer.guessContentTypes(mount);
-    },
-
-    _onMountRemoved: function(monitor, mount) {
-        this._transDispatcher.removeMount(mount);
-        this._residentSource.removeMount(mount);
-    },
-
-    ejectMount: function(mount) {
-        let mountOp = new ShellMountOperation.ShellMountOperation(mount);
-
-        // first, see if we have a drive
-        let drive = mount.get_drive();
-        let volume = mount.get_volume();
-
-        if (drive &&
-            drive.get_start_stop_type() == Gio.DriveStartStopType.SHUTDOWN &&
-            drive.can_stop()) {
-            drive.stop(0, mountOp.mountOp, null,
-                       Lang.bind(this, this._onStop));
-        } else {
-            if (mount.can_eject()) {
-                mount.eject_with_operation(0, mountOp.mountOp, null,
-                                           Lang.bind(this, this._onEject));
-            } else if (volume && volume.can_eject()) {
-                volume.eject_with_operation(0, mountOp.mountOp, null,
-                                            Lang.bind(this, this._onEject));
-            } else if (drive && drive.can_eject()) {
-                drive.eject_with_operation(0, mountOp.mountOp, null,
-                                           Lang.bind(this, this._onEject));
-            } else if (mount.can_unmount()) {
-                mount.unmount_with_operation(0, mountOp.mountOp, null,
-                                             Lang.bind(this, this._onUnmount));
-            }
-        }
-    },
-
-    _onUnmount: function(mount, res) {
-        try {
-            mount.unmount_with_operation_finish(res);
-        } catch (e) {
-            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
-                log('Unable to eject the mount ' + mount.get_name() 
-                    + ': ' + e.toString());
-        }
-    },
-
-    _onEject: function(source, res) {
-        try {
-            source.eject_with_operation_finish(res);
-        } catch (e) {
-            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
-                log('Unable to eject the drive ' + source.get_name()
-                    + ': ' + e.toString());
-        }
-    },
-
-    _onStop: function(drive, res) {
-        try {
-            drive.stop_finish(res);
-        } catch (e) {
-            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
-                log('Unable to stop the drive ' + drive.get_name() 
-                    + ': ' + e.toString());
-        }
-    },
-});
-
-const AutorunResidentSource = new Lang.Class({
-    Name: 'AutorunResidentSource',
-    Extends: MessageTray.Source,
-
-    _init: function() {
-        this.parent(_("Removable Devices"), 'media-removable');
-        this.showInLockScreen = false;
-
-        this._mounts = [];
-
-        this._notification = new AutorunResidentNotification(this);
-    },
-
-    addMount: function(mount, apps) {
-        if (!shouldAutorunMount(mount, false))
-            return;
-
-        let filtered = this._mounts.filter(function (element) {
-            return (element.mount == mount);
-        });
-
-        if (filtered.length != 0)
-            return;
-
-        let element = { mount: mount, apps: apps };
-        this._mounts.push(element);
-        this._redisplay();
-    },
-
-    removeMount: function(mount) {
-        this._mounts =
-            this._mounts.filter(function (element) {
-                return (element.mount != mount);
-            });
-
-        this._redisplay();
-    },
-
-    _redisplay: function() {
-        if (this._mounts.length == 0) {
-            this._notification.destroy();
-            this.destroy();
-
-            return;
-        }
-
-        this._notification.updateForMounts(this._mounts);
-
-        // add ourselves as a source, and push the notification
-        if (!Main.messageTray.contains(this)) {
-            Main.messageTray.add(this);
-            this.pushNotification(this._notification);
-        }
-    }
-});
-
-const AutorunResidentNotification = new Lang.Class({
-    Name: 'AutorunResidentNotification',
-    Extends: MessageTray.Notification,
-
-    _init: function(source) {
-        this.parent(source, source.title, null, { customContent: true });
-
-        // set the notification as resident
-        this.setResident(true);
-
-        this._layout = new St.BoxLayout ({ style_class: 'hotplug-resident-box',
-                                           vertical: true });
-
-        this.addActor(this._layout,
-                      { x_expand: true,
-                        x_fill: true });
-    },
-
-    updateForMounts: function(mounts) {
-        // remove all the layout content
-        this._layout.destroy_all_children();
-
-        for (let idx = 0; idx < mounts.length; idx++) {
-            let element = mounts[idx];
-
-            let actor = this._itemForMount(element.mount, element.apps);
-            this._layout.add(actor, { x_fill: true,
-                                      expand: true });
-        }
-    },
-
-    _itemForMount: function(mount, apps) {
-        let item = new St.BoxLayout();
-
-        // prepare the mount button content
-        let mountLayout = new St.BoxLayout();
-
-        let mountIcon = new St.Icon({ gicon: mount.get_icon(),
-                                      style_class: 'hotplug-resident-mount-icon' });
-        mountLayout.add_actor(mountIcon);
-
-        let labelBin = new St.Bin({ y_align: St.Align.MIDDLE });
-        let mountLabel =
-            new St.Label({ text: mount.get_name(),
-                           style_class: 'hotplug-resident-mount-label',
-                           track_hover: true,
-                           reactive: true });
-        labelBin.add_actor(mountLabel);
-        mountLayout.add_actor(labelBin);
-
-        let mountButton = new St.Button({ child: mountLayout,
-                                          x_align: St.Align.START,
-                                          x_fill: true,
-                                          style_class: 'hotplug-resident-mount',
-                                          button_mask: St.ButtonMask.ONE });
-        item.add(mountButton, { x_align: St.Align.START,
-                                expand: true });
-
-        let ejectIcon = 
-            new St.Icon({ icon_name: 'media-eject',
-                          style_class: 'hotplug-resident-eject-icon' });
-
-        let ejectButton =
-            new St.Button({ style_class: 'hotplug-resident-eject-button',
-                            button_mask: St.ButtonMask.ONE,
-                            child: ejectIcon });
-        item.add(ejectButton, { x_align: St.Align.END });
-
-        // now connect signals
-        mountButton.connect('clicked', Lang.bind(this, function(actor, event) {
-            startAppForMount(apps[0], mount);
-        }));
-
-        ejectButton.connect('clicked', Lang.bind(this, function() {
-            Main.autorunManager.ejectMount(mount);
-        }));
-
-        return item;
-    },
-});
-
-const AutorunTransientDispatcher = new Lang.Class({
-    Name: 'AutorunTransientDispatcher',
-
-    _init: function() {
-        this._sources = [];
-        this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
-    },
-
-    _getAutorunSettingForType: function(contentType) {
-        let runApp = this._settings.get_strv(SETTING_START_APP);
-        if (runApp.indexOf(contentType) != -1)
-            return AutorunSetting.RUN;
-
-        let ignore = this._settings.get_strv(SETTING_IGNORE);
-        if (ignore.indexOf(contentType) != -1)
-            return AutorunSetting.IGNORE;
-
-        let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
-        if (openFiles.indexOf(contentType) != -1)
-            return AutorunSetting.FILES;
-
-        return AutorunSetting.ASK;
-    },
-
-    _getSourceForMount: function(mount) {
-        let filtered =
-            this._sources.filter(function (source) {
-                return (source.mount == mount);
-            });
-
-        // we always make sure not to add two sources for the same
-        // mount in addMount(), so it's safe to assume filtered.length
-        // is always either 1 or 0.
-        if (filtered.length == 1)
-            return filtered[0];
-
-        return null;
-    },
-
-    _addSource: function(mount, apps) {
-        // if we already have a source showing for this 
-        // mount, return
-        if (this._getSourceForMount(mount))
-            return;
-     
-        // add a new source
-        this._sources.push(new AutorunTransientSource(mount, apps));
-    },
-
-    addMount: function(mount, apps, contentTypes) {
-        // if autorun is disabled globally, return
-        if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
-            return;
-
-        // if the mount doesn't want to be autorun, return
-        if (!shouldAutorunMount(mount, true))
-            return;
-
-        let setting = this._getAutorunSettingForType(contentTypes[0]);
-
-        // check at the settings for the first content type
-        // to see whether we should ask
-        if (setting == AutorunSetting.IGNORE)
-            return; // return right away
-
-        let success = false;
-        let app = null;
-
-        if (setting == AutorunSetting.RUN) {
-            app = Gio.app_info_get_default_for_type(contentTypes[0], false);
-        } else if (setting == AutorunSetting.FILES) {
-            app = Gio.app_info_get_default_for_type('inode/directory', false);
-        }
-
-        if (app)
-            success = startAppForMount(app, mount);
-
-        // we fallback here also in case the settings did not specify 'ask',
-        // but we failed launching the default app or the default file manager
-        if (!success)
-            this._addSource(mount, apps);
-    },
-
-    removeMount: function(mount) {
-        let source = this._getSourceForMount(mount);
-        
-        // if we aren't tracking this mount, don't do anything
-        if (!source)
-            return;
-
-        // destroy the notification source
-        source.destroy();
-    }
-});
-
-const AutorunTransientSource = new Lang.Class({
-    Name: 'AutorunTransientSource',
-    Extends: MessageTray.Source,
-
-    _init: function(mount, apps) {
-        this.mount = mount;
-        this.apps = apps;
-
-        this.parent(mount.get_name());
-
-        this._notification = new AutorunTransientNotification(this);
-
-        // add ourselves as a source, and popup the notification
-        Main.messageTray.add(this);
-        this.notify(this._notification);
-    },
-
-    createIcon: function(size) {
-        return new St.Icon({ gicon: this.mount.get_icon(),
-                             icon_size: size });
-    }
-});
-
-const AutorunTransientNotification = new Lang.Class({
-    Name: 'AutorunTransientNotification',
-    Extends: MessageTray.Notification,
-
-    _init: function(source) {
-        this.parent(source, source.title, null, { customContent: true });
-
-        this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
-                                       vertical: true });
-        this.addActor(this._box);
-
-        this._mount = source.mount;
-
-        source.apps.forEach(Lang.bind(this, function (app) {
-            let actor = this._buttonForApp(app);
-
-            if (actor)
-                this._box.add(actor, { x_fill: true,
-                                       x_align: St.Align.START });
-        }));
-
-        this._box.add(this._buttonForEject(), { x_fill: true,
-                                                x_align: St.Align.START });
-
-        // set the notification to transient and urgent, so that it
-        // expands out
-        this.setTransient(true);
-        this.setUrgency(MessageTray.Urgency.CRITICAL);
-    },
-
-    _buttonForApp: function(app) {
-        let box = new St.BoxLayout();
-        let icon = new St.Icon({ gicon: app.get_icon(),
-                                 style_class: 'hotplug-notification-item-icon' });
-        box.add(icon);
-
-        let label = new St.Bin({ y_align: St.Align.MIDDLE,
-                                 child: new St.Label
-                                 ({ text: _("Open with %s").format(app.get_name()) })
-                               });
-        box.add(label);
-
-        let button = new St.Button({ child: box,
-                                     x_fill: true,
-                                     x_align: St.Align.START,
-                                     button_mask: St.ButtonMask.ONE,
-                                     style_class: 'hotplug-notification-item' });
-
-        button.connect('clicked', Lang.bind(this, function() {
-            startAppForMount(app, this._mount);
-            this.destroy();
-        }));
-
-        return button;
-    },
-
-    _buttonForEject: function() {
-        let box = new St.BoxLayout();
-        let icon = new St.Icon({ icon_name: 'media-eject',
-                                 style_class: 'hotplug-notification-item-icon' });
-        box.add(icon);
-
-        let label = new St.Bin({ y_align: St.Align.MIDDLE,
-                                 child: new St.Label
-                                 ({ text: _("Eject") })
-                               });
-        box.add(label);
-
-        let button = new St.Button({ child: box,
-                                     x_fill: true,
-                                     x_align: St.Align.START,
-                                     button_mask: St.ButtonMask.ONE,
-                                     style_class: 'hotplug-notification-item' });
-
-        button.connect('clicked', Lang.bind(this, function() {
-            Main.autorunManager.ejectMount(this._mount);
-        }));
-
-        return button;
-    }
-});
-
diff --git a/js/ui/components/__init__.js b/js/ui/components/__init__.js
new file mode 100644 (file)
index 0000000..763c87d
--- /dev/null
@@ -0,0 +1,61 @@
+
+const Lang = imports.lang;
+const Main = imports.ui.main;
+
+const ComponentManager = new Lang.Class({
+    Name: 'ComponentManager',
+
+    _init: function() {
+        this._allComponents = {};
+        this._enabledComponents = [];
+
+        Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated));
+        this._sessionUpdated();
+    },
+
+    _sessionUpdated: function() {
+        let newEnabledComponents = Main.sessionMode.components;
+
+        newEnabledComponents.filter(Lang.bind(this, function(name) {
+            return this._enabledComponents.indexOf(name) == -1;
+        })).forEach(Lang.bind(this, function(name) {
+            this._enableComponent(name);
+        }));
+
+        this._enabledComponents.filter(Lang.bind(this, function(name) {
+            return newEnabledComponents.indexOf(name) == -1;
+        })).forEach(Lang.bind(this, function(name) {
+            this._disableComponent(name);
+        }));
+
+        this._enabledComponents = newEnabledComponents;
+    },
+
+    _importComponent: function(name) {
+        let module = imports.ui.components[name];
+        return module.Component;
+    },
+
+    _ensureComponent: function(name) {
+        let component = this._allComponents[name];
+        if (component)
+            return component;
+
+        let constructor = this._importComponent(name);
+        component = new constructor();
+        this._allComponents[name] = component;
+        return component;
+    },
+
+    _enableComponent: function(name) {
+        let component = this._ensureComponent(name);
+        component.enable();
+    },
+
+    _disableComponent: function(name) {
+        let component = this._allComponents[name];
+        if (component == null)
+            return;
+        component.disable();
+    }
+});
diff --git a/js/ui/components/automountManager.js b/js/ui/components/automountManager.js
new file mode 100644 (file)
index 0000000..9081e80
--- /dev/null
@@ -0,0 +1,241 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Params = imports.misc.params;
+const Shell = imports.gi.Shell;
+
+const GnomeSession = imports.misc.gnomeSession;
+const LoginManager = imports.misc.loginManager;
+const Main = imports.ui.main;
+const ShellMountOperation = imports.ui.shellMountOperation;
+
+const GNOME_SESSION_AUTOMOUNT_INHIBIT = 16;
+
+// GSettings keys
+const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
+const SETTING_ENABLE_AUTOMOUNT = 'automount';
+
+const AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
+
+const AutomountManager = new Lang.Class({
+    Name: 'AutomountManager',
+
+    _init: function() {
+        this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
+        this._volumeQueue = [];
+        this._session = new GnomeSession.SessionManager();
+        this._session.connectSignal('InhibitorAdded',
+                                    Lang.bind(this, this._InhibitorsChanged));
+        this._session.connectSignal('InhibitorRemoved',
+                                    Lang.bind(this, this._InhibitorsChanged));
+        this._inhibited = false;
+
+        this._loginManager = LoginManager.getLoginManager();
+        this._volumeMonitor = Gio.VolumeMonitor.get();
+    },
+
+    enable: function() {
+        this._volumeAddedId = this._volumeMonitor.connect('volume-added', Lang.bind(this, this._onVolumeAdded));
+        this._volumeRemovedId = this._volumeMonitor.connect('volume-removed', Lang.bind(this, this._onVolumeRemoved));
+        this._driveConnectedId = this._volumeMonitor.connect('drive-connected', Lang.bind(this, this._onDriveConnected));
+        this._driveDisconnectedId = this._volumeMonitor.connect('drive-disconnected', Lang.bind(this, this._onDriveDisconnected));
+        this._driveEjectButtonId = this._volumeMonitor.connect('drive-eject-button', Lang.bind(this, this._onDriveEjectButton));
+
+        this._mountAllId = Mainloop.idle_add(Lang.bind(this, this._startupMountAll));
+    },
+
+    disable: function() {
+        this._volumeMonitor.disconnect(this._volumeAddedId);
+        this._volumeMonitor.disconnect(this._volumeRemovedId);
+        this._volumeMonitor.disconnect(this._driveConnectedId);
+        this._volumeMonitor.disconnect(this._driveDisconnectedId);
+        this._volumeMonitor.disconnect(this._driveEjectButtonId);
+
+        if (this._mountAllId > 0) {
+            Mainloop.source_remove(this._mountAllId);
+            this._mountAllId = 0;
+        }
+    },
+
+    _InhibitorsChanged: function(object, senderName, [inhibtor]) {
+        this._session.IsInhibitedRemote(GNOME_SESSION_AUTOMOUNT_INHIBIT,
+            Lang.bind(this,
+                function(result, error) {
+                    if (!error) {
+                        this._inhibited = result[0];
+                    }
+                }));
+    },
+
+    _startupMountAll: function() {
+        let volumes = this._volumeMonitor.get_volumes();
+        volumes.forEach(Lang.bind(this, function(volume) {
+            this._checkAndMountVolume(volume, { checkSession: false,
+                                                useMountOp: false,
+                                                allowAutorun: false });
+        }));
+
+        this._mountAllId = 0;
+        return false;
+    },
+
+    _onDriveConnected: function() {
+        // if we're not in the current ConsoleKit session,
+        // or screensaver is active, don't play sounds
+        if (!this._loginManager.sessionActive)
+            return;
+
+        global.play_theme_sound(0, 'device-added-media');
+    },
+
+    _onDriveDisconnected: function() {
+        // if we're not in the current ConsoleKit session,
+        // or screensaver is active, don't play sounds
+        if (!this._loginManager.sessionActive)
+            return;
+
+        global.play_theme_sound(0, 'device-removed-media');        
+    },
+
+    _onDriveEjectButton: function(monitor, drive) {
+        // TODO: this code path is not tested, as the GVfs volume monitor
+        // doesn't emit this signal just yet.
+        if (!this._loginManager.sessionActive)
+            return;
+
+        // we force stop/eject in this case, so we don't have to pass a
+        // mount operation object
+        if (drive.can_stop()) {
+            drive.stop
+                (Gio.MountUnmountFlags.FORCE, null, null,
+                 Lang.bind(this, function(drive, res) {
+                     try {
+                         drive.stop_finish(res);
+                     } catch (e) {
+                         log("Unable to stop the drive after drive-eject-button " + e.toString());
+                     }
+                 }));
+        } else if (drive.can_eject()) {
+            drive.eject_with_operation 
+                (Gio.MountUnmountFlags.FORCE, null, null,
+                 Lang.bind(this, function(drive, res) {
+                     try {
+                         drive.eject_with_operation_finish(res);
+                     } catch (e) {
+                         log("Unable to eject the drive after drive-eject-button " + e.toString());
+                     }
+                 }));
+        }
+    },
+
+    _onVolumeAdded: function(monitor, volume) {
+        this._checkAndMountVolume(volume);
+    },
+
+    _checkAndMountVolume: function(volume, params) {
+        params = Params.parse(params, { checkSession: true,
+                                        useMountOp: true,
+                                        allowAutorun: true });
+
+        if (params.checkSession) {
+            // if we're not in the current ConsoleKit session,
+            // don't attempt automount
+            if (!this._loginManager.sessionActive)
+                return;
+        }
+
+        if (this._inhibited)
+            return;
+
+        // Volume is already mounted, don't bother.
+        if (volume.get_mount())
+            return;
+
+        if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) ||
+            !volume.should_automount() ||
+            !volume.can_mount()) {
+            // allow the autorun to run anyway; this can happen if the
+            // mount gets added programmatically later, even if 
+            // should_automount() or can_mount() are false, like for
+            // blank optical media.
+            this._allowAutorun(volume);
+            this._allowAutorunExpire(volume);
+
+            return;
+        }
+
+        if (params.useMountOp) {
+            let operation = new ShellMountOperation.ShellMountOperation(volume);
+            this._mountVolume(volume, operation, params.allowAutorun);
+        } else {
+            this._mountVolume(volume, null, params.allowAutorun);
+        }
+    },
+
+    _mountVolume: function(volume, operation, allowAutorun) {
+        if (allowAutorun)
+            this._allowAutorun(volume);
+
+        let mountOp = operation ? operation.mountOp : null;
+        volume._operation = operation;
+
+        volume.mount(0, mountOp, null,
+                     Lang.bind(this, this._onVolumeMounted));
+    },
+
+    _onVolumeMounted: function(volume, res) {
+        this._allowAutorunExpire(volume);
+
+        try {
+            volume.mount_finish(res);
+            this._closeOperation(volume);
+        } catch (e) {
+            // FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks
+            // backend in this case, see 
+            // https://bugs.freedesktop.org/show_bug.cgi?id=51271
+            if (e.message.indexOf('No key available with this passphrase') != -1) {
+                this._reaskPassword(volume);
+            } else {
+                if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+                    log('Unable to mount volume ' + volume.get_name() + ': ' + e.toString());
+
+                this._closeOperation(volume);
+            }
+        }
+    },
+
+    _onVolumeRemoved: function(monitor, volume) {
+        this._volumeQueue = 
+            this._volumeQueue.filter(function(element) {
+                return (element != volume);
+            });
+    },
+
+    _reaskPassword: function(volume) {
+        let existingDialog = volume._operation ? volume._operation.borrowDialog() : null;
+        let operation = 
+            new ShellMountOperation.ShellMountOperation(volume,
+                                                        { existingDialog: existingDialog });
+        this._mountVolume(volume, operation);
+    },
+
+    _closeOperation: function(volume) {
+        if (volume._operation)
+            volume._operation.close();
+    },
+
+    _allowAutorun: function(volume) {
+        volume.allowAutorun = true;
+    },
+
+    _allowAutorunExpire: function(volume) {
+        Mainloop.timeout_add_seconds(AUTORUN_EXPIRE_TIMEOUT_SECS, function() {
+            volume.allowAutorun = false;
+            return false;
+        });
+    }
+});
+const Component = AutomountManager;
diff --git a/js/ui/components/autorunManager.js b/js/ui/components/autorunManager.js
new file mode 100644 (file)
index 0000000..3434e9b
--- /dev/null
@@ -0,0 +1,601 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Lang = imports.lang;
+const Gio = imports.gi.Gio;
+const St = imports.gi.St;
+
+const LoginManager = imports.misc.loginManager;
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+const ShellMountOperation = imports.ui.shellMountOperation;
+
+// GSettings keys
+const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
+const SETTING_DISABLE_AUTORUN = 'autorun-never';
+const SETTING_START_APP = 'autorun-x-content-start-app';
+const SETTING_IGNORE = 'autorun-x-content-ignore';
+const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
+
+const AutorunSetting = {
+    RUN: 0,
+    IGNORE: 1,
+    FILES: 2,
+    ASK: 3
+};
+
+// misc utils
+function shouldAutorunMount(mount, forTransient) {
+    let root = mount.get_root();
+    let volume = mount.get_volume();
+
+    if (!volume || (!volume.allowAutorun && forTransient))
+        return false;
+
+    if (!root.is_native() || isMountRootHidden(root))
+        return false;
+
+    return true;
+}
+
+function isMountRootHidden(root) {
+    let path = root.get_path();
+
+    // skip any mounts in hidden directory hierarchies
+    return (path.indexOf('/.') != -1);
+}
+
+function startAppForMount(app, mount) {
+    let files = [];
+    let root = mount.get_root();
+    let retval = false;
+
+    files.push(root);
+
+    try {
+        retval = app.launch(files, 
+                            global.create_app_launch_context())
+    } catch (e) {
+        log('Unable to launch the application ' + app.get_name()
+            + ': ' + e.toString());
+    }
+
+    return retval;
+}
+
+/******************************************/
+
+const HotplugSnifferIface = <interface name="org.gnome.Shell.HotplugSniffer">
+<method name="SniffURI">
+    <arg type="s" direction="in" />
+    <arg type="as" direction="out" />
+</method>
+</interface>;
+
+const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(HotplugSnifferIface);
+function HotplugSniffer() {
+    return new HotplugSnifferProxy(Gio.DBus.session,
+                                   'org.gnome.Shell.HotplugSniffer',
+                                   '/org/gnome/Shell/HotplugSniffer');
+}
+
+const ContentTypeDiscoverer = new Lang.Class({
+    Name: 'ContentTypeDiscoverer',
+
+    _init: function(callback) {
+        this._callback = callback;
+    },
+
+    guessContentTypes: function(mount) {
+        // guess mount's content types using GIO
+        mount.guess_content_type(false, null,
+                                 Lang.bind(this,
+                                           this._onContentTypeGuessed));
+    },
+
+    _onContentTypeGuessed: function(mount, res) {
+        let contentTypes = [];
+
+        try {
+            contentTypes = mount.guess_content_type_finish(res);
+        } catch (e) {
+            log('Unable to guess content types on added mount ' + mount.get_name()
+                + ': ' + e.toString());
+        }
+
+        if (contentTypes.length) {
+            this._emitCallback(mount, contentTypes);
+        } else {
+            let root = mount.get_root();
+
+            let hotplugSniffer = new HotplugSniffer();
+            hotplugSniffer.SniffURIRemote(root.get_uri(),
+                 Lang.bind(this, function([contentTypes]) {
+                     this._emitCallback(mount, contentTypes);
+                 }));
+        }
+    },
+
+    _emitCallback: function(mount, contentTypes) {
+        if (!contentTypes)
+            contentTypes = [];
+
+        // we're not interested in win32 software content types here
+        contentTypes = contentTypes.filter(function(type) {
+            return (type != 'x-content/win32-software');
+        });
+
+        let apps = [];
+        contentTypes.forEach(function(type) {
+            let app = Gio.app_info_get_default_for_type(type, false);
+
+            if (app)
+                apps.push(app);
+        });
+
+        if (apps.length == 0)
+            apps.push(Gio.app_info_get_default_for_type('inode/directory', false));
+
+        this._callback(mount, apps, contentTypes);
+    }
+});
+
+const AutorunManager = new Lang.Class({
+    Name: 'AutorunManager',
+
+    _init: function() {
+        this._loginManager = LoginManager.getLoginManager();
+
+        this._volumeMonitor = Gio.VolumeMonitor.get();
+
+        this._transDispatcher = new AutorunTransientDispatcher(this);
+    },
+
+    enable: function() {
+        this._residentSource = new AutorunResidentSource(this);
+        this._scanMounts();
+
+        this._mountAddedId = this._volumeMonitor.connect('mount-added', Lang.bind(this, this._onMountAdded));
+        this._mountRemovedId = this._volumeMonitor.connect('mount-removed', Lang.bind(this, this._onMountRemoved));
+    },
+
+    disable: function() {
+        this._residentSource.destroy();
+        this._volumeMonitor.disconnect(this._mountAddedId);
+        this._volumeMonitor.disconnect(this._mountRemovedId);
+    },
+
+    _scanMounts: function() {
+        let mounts = this._volumeMonitor.get_mounts();
+        mounts.forEach(Lang.bind(this, function (mount) {
+            let discoverer = new ContentTypeDiscoverer(Lang.bind (this, 
+                function (mount, apps) {
+                    this._residentSource.addMount(mount, apps);
+                }));
+
+            discoverer.guessContentTypes(mount);
+        }));
+    },
+
+    _onMountAdded: function(monitor, mount) {
+        // don't do anything if our session is not the currently
+        // active one
+        if (!this._loginManager.sessionActive)
+            return;
+
+        let discoverer = new ContentTypeDiscoverer(Lang.bind (this,
+            function (mount, apps, contentTypes) {
+                this._transDispatcher.addMount(mount, apps, contentTypes);
+                this._residentSource.addMount(mount, apps);
+            }));
+
+        discoverer.guessContentTypes(mount);
+    },
+
+    _onMountRemoved: function(monitor, mount) {
+        this._transDispatcher.removeMount(mount);
+        this._residentSource.removeMount(mount);
+    },
+
+    ejectMount: function(mount) {
+        let mountOp = new ShellMountOperation.ShellMountOperation(mount);
+
+        // first, see if we have a drive
+        let drive = mount.get_drive();
+        let volume = mount.get_volume();
+
+        if (drive &&
+            drive.get_start_stop_type() == Gio.DriveStartStopType.SHUTDOWN &&
+            drive.can_stop()) {
+            drive.stop(0, mountOp.mountOp, null,
+                       Lang.bind(this, this._onStop));
+        } else {
+            if (mount.can_eject()) {
+                mount.eject_with_operation(0, mountOp.mountOp, null,
+                                           Lang.bind(this, this._onEject));
+            } else if (volume && volume.can_eject()) {
+                volume.eject_with_operation(0, mountOp.mountOp, null,
+                                            Lang.bind(this, this._onEject));
+            } else if (drive && drive.can_eject()) {
+                drive.eject_with_operation(0, mountOp.mountOp, null,
+                                           Lang.bind(this, this._onEject));
+            } else if (mount.can_unmount()) {
+                mount.unmount_with_operation(0, mountOp.mountOp, null,
+                                             Lang.bind(this, this._onUnmount));
+            }
+        }
+    },
+
+    _onUnmount: function(mount, res) {
+        try {
+            mount.unmount_with_operation_finish(res);
+        } catch (e) {
+            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+                log('Unable to eject the mount ' + mount.get_name() 
+                    + ': ' + e.toString());
+        }
+    },
+
+    _onEject: function(source, res) {
+        try {
+            source.eject_with_operation_finish(res);
+        } catch (e) {
+            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+                log('Unable to eject the drive ' + source.get_name()
+                    + ': ' + e.toString());
+        }
+    },
+
+    _onStop: function(drive, res) {
+        try {
+            drive.stop_finish(res);
+        } catch (e) {
+            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+                log('Unable to stop the drive ' + drive.get_name() 
+                    + ': ' + e.toString());
+        }
+    },
+});
+
+const AutorunResidentSource = new Lang.Class({
+    Name: 'AutorunResidentSource',
+    Extends: MessageTray.Source,
+
+    _init: function(manager) {
+        this.parent(_("Removable Devices"), 'media-removable');
+        this.showInLockScreen = false;
+
+        this._mounts = [];
+
+        this._notification = new AutorunResidentNotification(this._manager, this);
+        this._manager = manager;
+    },
+
+    addMount: function(mount, apps) {
+        if (!shouldAutorunMount(mount, false))
+            return;
+
+        let filtered = this._mounts.filter(function (element) {
+            return (element.mount == mount);
+        });
+
+        if (filtered.length != 0)
+            return;
+
+        let element = { mount: mount, apps: apps };
+        this._mounts.push(element);
+        this._redisplay();
+    },
+
+    removeMount: function(mount) {
+        this._mounts =
+            this._mounts.filter(function (element) {
+                return (element.mount != mount);
+            });
+
+        this._redisplay();
+    },
+
+    _redisplay: function() {
+        if (this._mounts.length == 0) {
+            this._notification.destroy();
+            this.destroy();
+
+            return;
+        }
+
+        this._notification.updateForMounts(this._mounts);
+
+        // add ourselves as a source, and push the notification
+        if (!Main.messageTray.contains(this)) {
+            Main.messageTray.add(this);
+            this.pushNotification(this._notification);
+        }
+    }
+});
+
+const AutorunResidentNotification = new Lang.Class({
+    Name: 'AutorunResidentNotification',
+    Extends: MessageTray.Notification,
+
+    _init: function(manager, source) {
+        this.parent(source, source.title, null, { customContent: true });
+
+        // set the notification as resident
+        this.setResident(true);
+
+        this._layout = new St.BoxLayout ({ style_class: 'hotplug-resident-box',
+                                           vertical: true });
+        this._manager = manager;
+
+        this.addActor(this._layout,
+                      { x_expand: true,
+                        x_fill: true });
+    },
+
+    updateForMounts: function(mounts) {
+        // remove all the layout content
+        this._layout.destroy_all_children();
+
+        for (let idx = 0; idx < mounts.length; idx++) {
+            let element = mounts[idx];
+
+            let actor = this._itemForMount(element.mount, element.apps);
+            this._layout.add(actor, { x_fill: true,
+                                      expand: true });
+        }
+    },
+
+    _itemForMount: function(mount, apps) {
+        let item = new St.BoxLayout();
+
+        // prepare the mount button content
+        let mountLayout = new St.BoxLayout();
+
+        let mountIcon = new St.Icon({ gicon: mount.get_icon(),
+                                      style_class: 'hotplug-resident-mount-icon' });
+        mountLayout.add_actor(mountIcon);
+
+        let labelBin = new St.Bin({ y_align: St.Align.MIDDLE });
+        let mountLabel =
+            new St.Label({ text: mount.get_name(),
+                           style_class: 'hotplug-resident-mount-label',
+                           track_hover: true,
+                           reactive: true });
+        labelBin.add_actor(mountLabel);
+        mountLayout.add_actor(labelBin);
+
+        let mountButton = new St.Button({ child: mountLayout,
+                                          x_align: St.Align.START,
+                                          x_fill: true,
+                                          style_class: 'hotplug-resident-mount',
+                                          button_mask: St.ButtonMask.ONE });
+        item.add(mountButton, { x_align: St.Align.START,
+                                expand: true });
+
+        let ejectIcon = 
+            new St.Icon({ icon_name: 'media-eject',
+                          style_class: 'hotplug-resident-eject-icon' });
+
+        let ejectButton =
+            new St.Button({ style_class: 'hotplug-resident-eject-button',
+                            button_mask: St.ButtonMask.ONE,
+                            child: ejectIcon });
+        item.add(ejectButton, { x_align: St.Align.END });
+
+        // now connect signals
+        mountButton.connect('clicked', Lang.bind(this, function(actor, event) {
+            this._manager.startAppForMount(apps[0], mount);
+        }));
+
+        ejectButton.connect('clicked', Lang.bind(this, function() {
+            this._manager.ejectMount(mount);
+        }));
+
+        return item;
+    },
+});
+
+const AutorunTransientDispatcher = new Lang.Class({
+    Name: 'AutorunTransientDispatcher',
+
+    _init: function(manager) {
+        this._manager = manager;
+        this._sources = [];
+        this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
+    },
+
+    _getAutorunSettingForType: function(contentType) {
+        let runApp = this._settings.get_strv(SETTING_START_APP);
+        if (runApp.indexOf(contentType) != -1)
+            return AutorunSetting.RUN;
+
+        let ignore = this._settings.get_strv(SETTING_IGNORE);
+        if (ignore.indexOf(contentType) != -1)
+            return AutorunSetting.IGNORE;
+
+        let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
+        if (openFiles.indexOf(contentType) != -1)
+            return AutorunSetting.FILES;
+
+        return AutorunSetting.ASK;
+    },
+
+    _getSourceForMount: function(mount) {
+        let filtered =
+            this._sources.filter(function (source) {
+                return (source.mount == mount);
+            });
+
+        // we always make sure not to add two sources for the same
+        // mount in addMount(), so it's safe to assume filtered.length
+        // is always either 1 or 0.
+        if (filtered.length == 1)
+            return filtered[0];
+
+        return null;
+    },
+
+    _addSource: function(mount, apps) {
+        // if we already have a source showing for this 
+        // mount, return
+        if (this._getSourceForMount(mount))
+            return;
+     
+        // add a new source
+        this._sources.push(new AutorunTransientSource(this._manager, mount, apps));
+    },
+
+    addMount: function(mount, apps, contentTypes) {
+        // if autorun is disabled globally, return
+        if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
+            return;
+
+        // if the mount doesn't want to be autorun, return
+        if (!shouldAutorunMount(mount, true))
+            return;
+
+        let setting = this._getAutorunSettingForType(contentTypes[0]);
+
+        // check at the settings for the first content type
+        // to see whether we should ask
+        if (setting == AutorunSetting.IGNORE)
+            return; // return right away
+
+        let success = false;
+        let app = null;
+
+        if (setting == AutorunSetting.RUN) {
+            app = Gio.app_info_get_default_for_type(contentTypes[0], false);
+        } else if (setting == AutorunSetting.FILES) {
+            app = Gio.app_info_get_default_for_type('inode/directory', false);
+        }
+
+        if (app)
+            success = startAppForMount(app, mount);
+
+        // we fallback here also in case the settings did not specify 'ask',
+        // but we failed launching the default app or the default file manager
+        if (!success)
+            this._addSource(mount, apps);
+    },
+
+    removeMount: function(mount) {
+        let source = this._getSourceForMount(mount);
+        
+        // if we aren't tracking this mount, don't do anything
+        if (!source)
+            return;
+
+        // destroy the notification source
+        source.destroy();
+    }
+});
+
+const AutorunTransientSource = new Lang.Class({
+    Name: 'AutorunTransientSource',
+    Extends: MessageTray.Source,
+
+    _init: function(manager, mount, apps) {
+        this._manager = manager;
+        this.mount = mount;
+        this.apps = apps;
+
+        this.parent(mount.get_name());
+
+        this._notification = new AutorunTransientNotification(this._manager, this);
+
+        // add ourselves as a source, and popup the notification
+        Main.messageTray.add(this);
+        this.notify(this._notification);
+    },
+
+    createIcon: function(size) {
+        return new St.Icon({ gicon: this.mount.get_icon(),
+                             icon_size: size });
+    }
+});
+
+const AutorunTransientNotification = new Lang.Class({
+    Name: 'AutorunTransientNotification',
+    Extends: MessageTray.Notification,
+
+    _init: function(manager, source) {
+        this.parent(source, source.title, null, { customContent: true });
+
+        this._manager = manager;
+        this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
+                                       vertical: true });
+        this.addActor(this._box);
+
+        this._mount = source.mount;
+
+        source.apps.forEach(Lang.bind(this, function (app) {
+            let actor = this._buttonForApp(app);
+
+            if (actor)
+                this._box.add(actor, { x_fill: true,
+                                       x_align: St.Align.START });
+        }));
+
+        this._box.add(this._buttonForEject(), { x_fill: true,
+                                                x_align: St.Align.START });
+
+        // set the notification to transient and urgent, so that it
+        // expands out
+        this.setTransient(true);
+        this.setUrgency(MessageTray.Urgency.CRITICAL);
+    },
+
+    _buttonForApp: function(app) {
+        let box = new St.BoxLayout();
+        let icon = new St.Icon({ gicon: app.get_icon(),
+                                 style_class: 'hotplug-notification-item-icon' });
+        box.add(icon);
+
+        let label = new St.Bin({ y_align: St.Align.MIDDLE,
+                                 child: new St.Label
+                                 ({ text: _("Open with %s").format(app.get_name()) })
+                               });
+        box.add(label);
+
+        let button = new St.Button({ child: box,
+                                     x_fill: true,
+                                     x_align: St.Align.START,
+                                     button_mask: St.ButtonMask.ONE,
+                                     style_class: 'hotplug-notification-item' });
+
+        button.connect('clicked', Lang.bind(this, function() {
+            startAppForMount(app, this._mount);
+            this.destroy();
+        }));
+
+        return button;
+    },
+
+    _buttonForEject: function() {
+        let box = new St.BoxLayout();
+        let icon = new St.Icon({ icon_name: 'media-eject',
+                                 style_class: 'hotplug-notification-item-icon' });
+        box.add(icon);
+
+        let label = new St.Bin({ y_align: St.Align.MIDDLE,
+                                 child: new St.Label
+                                 ({ text: _("Eject") })
+                               });
+        box.add(label);
+
+        let button = new St.Button({ child: box,
+                                     x_fill: true,
+                                     x_align: St.Align.START,
+                                     button_mask: St.ButtonMask.ONE,
+                                     style_class: 'hotplug-notification-item' });
+
+        button.connect('clicked', Lang.bind(this, function() {
+            this._manager.ejectMount(this._mount);
+        }));
+
+        return button;
+    }
+});
+
+const Component = AutorunManager;
diff --git a/js/ui/components/keyring.js b/js/ui/components/keyring.js
new file mode 100644 (file)
index 0000000..dfa7cc0
--- /dev/null
@@ -0,0 +1,226 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Lang = imports.lang;
+const Shell = imports.gi.Shell;
+const Clutter = imports.gi.Clutter;
+const St = imports.gi.St;
+const Pango = imports.gi.Pango;
+const Gio = imports.gi.Gio;
+const GObject = imports.gi.GObject;
+const Gcr = imports.gi.Gcr;
+
+const ModalDialog = imports.ui.modalDialog;
+const ShellEntry = imports.ui.shellEntry;
+const CheckBox = imports.ui.checkBox;
+
+let prompter = null;
+
+const KeyringDialog = new Lang.Class({
+    Name: 'KeyringDialog',
+    Extends: ModalDialog.ModalDialog,
+
+    _init: function() {
+        this.parent({ styleClass: 'prompt-dialog' });
+
+        this.prompt = new Shell.KeyringPrompt();
+        this.prompt.connect('show-password', Lang.bind(this, this._onShowPassword));
+        this.prompt.connect('show-confirm', Lang.bind(this, this._onShowConfirm));
+        this.prompt.connect('hide-prompt', Lang.bind(this, this._onHidePrompt));
+
+        let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
+                                                vertical: false });
+        this.contentLayout.add(mainContentBox);
+
+        let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
+        mainContentBox.add(icon,
+                           { x_fill:  true,
+                             y_fill:  false,
+                             x_align: St.Align.END,
+                             y_align: St.Align.START });
+
+        this._messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
+                                              vertical: true });
+        mainContentBox.add(this._messageBox,
+                           { y_align: St.Align.START, expand: true, x_fill: true, y_fill: true });
+
+        let subject = new St.Label({ style_class: 'prompt-dialog-headline' });
+        this.prompt.bind_property('message', subject, 'text', GObject.BindingFlags.SYNC_CREATE);
+
+        this._messageBox.add(subject,
+                             { y_fill:  false,
+                               y_align: St.Align.START });
+
+        let description = new St.Label({ style_class: 'prompt-dialog-description' });
+        description.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        description.clutter_text.line_wrap = true;
+        this.prompt.bind_property('description', description, 'text', GObject.BindingFlags.SYNC_CREATE);
+        this._messageBox.add(description,
+                            { y_fill:  true,
+                              y_align: St.Align.START });
+
+        this._controlTable = null;
+
+        let buttons = [{ label: '',
+                         action: Lang.bind(this, this._onCancelButton),
+                         key:    Clutter.Escape
+                       },
+                       { label: '',
+                         action: Lang.bind(this, this._onContinueButton),
+                         default: true
+                       }]
+
+        this.setButtons(buttons);
+        this._cancelButton = buttons[0].button;
+        this._continueButton = buttons[1].button;
+
+        this.prompt.bind_property('cancel-label', this._cancelButton, 'label', GObject.BindingFlags.SYNC_CREATE);
+        this.prompt.bind_property('continue-label', this._continueButton, 'label', GObject.BindingFlags.SYNC_CREATE);
+    },
+
+    _buildControlTable: function() {
+        let table = new St.Table({ style_class: 'keyring-dialog-control-table' });
+        let row = 0;
+
+        if (this.prompt.password_visible) {
+            let label = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
+            label.set_text(_("Password:"));
+            table.add(label, { row: row, col: 0,
+                               x_expand: false, x_fill: true,
+                               x_align: St.Align.START,
+                               y_fill: false, y_align: St.Align.MIDDLE });
+            this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
+                                                 text: '',
+                                                 can_focus: true});
+            this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
+            ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
+            this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onPasswordActivate));
+            table.add(this._passwordEntry, { row: row, col: 1, x_expand: true, x_fill: true, x_align: St.Align.START });
+            row++;
+        } else {
+            this._passwordEntry = null;
+        }
+
+        if (this.prompt.confirm_visible) {
+            var label = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
+            label.set_text(_("Type again:"));
+            table.add(label, { row: row, col: 0,
+                               x_expand: false, x_fill: true,
+                               x_align: St.Align.START,
+                               y_fill: false, y_align: St.Align.MIDDLE });
+            this._confirmEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
+                                                text: '',
+                                                can_focus: true});
+            this._confirmEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
+            ShellEntry.addContextMenu(this._confirmEntry, { isPassword: true });
+            this._confirmEntry.clutter_text.connect('activate', Lang.bind(this, this._onConfirmActivate));
+            table.add(this._confirmEntry, { row: row, col: 1, x_expand: true, x_fill: true, x_align: St.Align.START });
+            row++;
+        } else {
+            this._confirmEntry = null;
+        }
+
+        this.prompt.set_password_actor(this._passwordEntry ? this._passwordEntry.clutter_text : null);
+        this.prompt.set_confirm_actor(this._confirmEntry ? this._confirmEntry.clutter_text : null);
+
+        if (this.prompt.choice_visible) {
+            let choice = new CheckBox.CheckBox();
+            this.prompt.bind_property('choice-label', choice.getLabelActor(), 'text', GObject.BindingFlags.SYNC_CREATE);
+            this.prompt.bind_property('choice-chosen', choice.actor, 'checked', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);
+            table.add(choice.actor, { row: row, col: 1, x_expand: false, x_fill: true, x_align: St.Align.START });
+            row++;
+        }
+
+        let warning = new St.Label({ style_class: 'prompt-dialog-error-label' });
+        warning.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        warning.clutter_text.line_wrap = true;
+        table.add(warning, { row: row, col: 1, x_expand: false, x_fill: false, x_align: St.Align.START });
+        this.prompt.bind_property('warning-visible', warning, 'visible', GObject.BindingFlags.SYNC_CREATE);
+        this.prompt.bind_property('warning', warning, 'text', GObject.BindingFlags.SYNC_CREATE);
+
+        if (this._controlTable) {
+            this._controlTable.destroy_all_children();
+            this._controlTable.destroy();
+        }
+
+        this._controlTable = table;
+        this._messageBox.add(table, { x_fill: true, y_fill: true });
+    },
+
+    _ensureOpen: function() {
+        // NOTE: ModalDialog.open() is safe to call if the dialog is
+        // already open - it just returns true without side-effects
+        if (this.open())
+          return true;
+
+        // The above fail if e.g. unable to get input grab
+        //
+        // In an ideal world this wouldn't happen (because the
+        // Shell is in complete control of the session) but that's
+        // just not how things work right now.
+
+        log('keyringPrompt: Failed to show modal dialog.' +
+            ' Dismissing prompt request');
+        this.prompt.cancel()
+        return false;
+    },
+
+    _onShowPassword: function(prompt) {
+        this._buildControlTable();
+        this._ensureOpen();
+        this._passwordEntry.grab_key_focus();
+    },
+
+    _onShowConfirm: function(prompt) {
+        this._buildControlTable();
+        this._ensureOpen();
+        this._continueButton.grab_key_focus();
+    },
+
+    _onHidePrompt: function(prompt) {
+        this.close();
+    },
+
+    _onPasswordActivate: function() {
+        if (this.prompt.confirm_visible)
+            this._confirmEntry.grab_key_focus();
+        else
+            this._onContinueButton();
+    },
+
+    _onConfirmActivate: function() {
+        this._onContinueButton();
+    },
+
+    _onContinueButton: function() {
+        this.prompt.complete();
+    },
+
+    _onCancelButton: function() {
+        this.prompt.cancel();
+    },
+});
+
+const KeyringPrompter = new Lang.Class({
+    Name: 'KeyringPrompter',
+
+    _init: function() {
+        this._prompter = new Gcr.SystemPrompter();
+        this._prompter.connect('new-prompt', function(prompter) {
+            let dialog = new KeyringDialog();
+            return dialog.prompt;
+        });
+        this._dbusId = null;
+    },
+
+    enable: function() {
+        this._prompter.register(Gio.DBus.session);
+        this._dbusId = Gio.DBus.session.own_name('org.gnome.keyring.SystemPrompter',
+                                                 Gio.BusNameOwnerFlags.REPLACE, null, null);
+    },
+
+    disable: function() {
+        Gio.DBus.session.unown_name(this._dbusId);
+    }
+});
+
+const Component = KeyringPrompter;
diff --git a/js/ui/components/networkAgent.js b/js/ui/components/networkAgent.js
new file mode 100644 (file)
index 0000000..2eaca0c
--- /dev/null
@@ -0,0 +1,694 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Lang = imports.lang;
+const NetworkManager = imports.gi.NetworkManager;
+const NMClient = imports.gi.NMClient;
+const Pango = imports.gi.Pango;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+
+const Config = imports.misc.config;
+const ModalDialog = imports.ui.modalDialog;
+const PopupMenu = imports.ui.popupMenu;
+const ShellEntry = imports.ui.shellEntry;
+
+const VPN_UI_GROUP = 'VPN Plugin UI';
+
+const NetworkSecretDialog = new Lang.Class({
+    Name: 'NetworkSecretDialog',
+    Extends: ModalDialog.ModalDialog,
+
+    _init: function(agent, requestId, connection, settingName, hints, contentOverride) {
+        this.parent({ styleClass: 'prompt-dialog' });
+
+        this._agent = agent;
+        this._requestId = requestId;
+        this._connection = connection;
+        this._settingName = settingName;
+        this._hints = hints;
+
+        if (contentOverride)
+            this._content = contentOverride;
+        else
+            this._content = this._getContent();
+
+        let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
+                                                vertical: false });
+        this.contentLayout.add(mainContentBox,
+                               { x_fill: true,
+                                 y_fill: true });
+
+        let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
+        mainContentBox.add(icon,
+                           { x_fill:  true,
+                             y_fill:  false,
+                             x_align: St.Align.END,
+                             y_align: St.Align.START });
+
+        let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
+                                            vertical: true });
+        mainContentBox.add(messageBox,
+                           { y_align: St.Align.START });
+
+        let subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
+                                            text: this._content.title });
+        messageBox.add(subjectLabel,
+                       { y_fill:  false,
+                         y_align: St.Align.START });
+
+        if (this._content.message != null) {
+            let descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
+                                                  text: this._content.message,
+                                                  // HACK: for reasons unknown to me, the label
+                                                  // is not asked the correct height for width,
+                                                  // and thus is underallocated
+                                                  // place a fixed height to avoid overflowing
+                                                  style: 'height: 3em'
+                                                });
+            descriptionLabel.clutter_text.line_wrap = true;
+
+            messageBox.add(descriptionLabel,
+                           { y_fill:  true,
+                             y_align: St.Align.START,
+                             expand: true });
+        }
+
+        let secretTable = new St.Table({ style_class: 'network-dialog-secret-table' });
+        let initialFocusSet = false;
+        let pos = 0;
+        for (let i = 0; i < this._content.secrets.length; i++) {
+            let secret = this._content.secrets[i];
+            let label = new St.Label({ style_class: 'prompt-dialog-password-label',
+                                       text: secret.label });
+
+            let reactive = secret.key != null;
+
+            secret.entry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
+                                          text: secret.value, can_focus: reactive,
+                                          reactive: reactive });
+            ShellEntry.addContextMenu(secret.entry,
+                                      { isPassword: secret.password });
+
+            if (secret.validate)
+                secret.valid = secret.validate(secret);
+            else // no special validation, just ensure it's not empty
+                secret.valid = secret.value.length > 0;
+
+            if (reactive) {
+                if (!initialFocusSet) {
+                    this.setInitialKeyFocus(secret.entry);
+                    initialFocusSet = true;
+                }
+
+                secret.entry.clutter_text.connect('activate', Lang.bind(this, this._onOk));
+                secret.entry.clutter_text.connect('text-changed', Lang.bind(this, function() {
+                    secret.value = secret.entry.get_text();
+                    if (secret.validate)
+                        secret.valid = secret.validate(secret);
+                    else
+                        secret.valid = secret.value.length > 0;
+                    this._updateOkButton();
+                }));
+            } else
+                secret.valid = true;
+
+            secretTable.add(label, { row: pos, col: 0,
+                                     x_expand: false, x_fill: true,
+                                     x_align: St.Align.START,
+                                     y_fill: false, y_align: St.Align.MIDDLE });
+            secretTable.add(secret.entry, { row: pos, col: 1, x_expand: true, x_fill: true, y_align: St.Align.END });
+            pos++;
+
+            if (secret.password)
+                secret.entry.clutter_text.set_password_char('\u25cf');
+        }
+
+        messageBox.add(secretTable);
+
+        this._okButton = { label:  _("Connect"),
+                           action: Lang.bind(this, this._onOk),
+                           default: true
+                         };
+
+        this.setButtons([{ label: _("Cancel"),
+                           action: Lang.bind(this, this.cancel),
+                           key:    Clutter.KEY_Escape,
+                         },
+                         this._okButton]);
+    },
+
+    _updateOkButton: function() {
+        let valid = true;
+        for (let i = 0; i < this._content.secrets.length; i++) {
+            let secret = this._content.secrets[i];
+            valid = valid && secret.valid;
+        }
+
+        this._okButton.button.reactive = valid;
+        this._okButton.button.can_focus = valid;
+    },
+
+    _onOk: function() {
+        let valid = true;
+        for (let i = 0; i < this._content.secrets.length; i++) {
+            let secret = this._content.secrets[i];
+            valid = valid && secret.valid;
+            if (secret.key != null)
+                this._agent.set_password(this._requestId, secret.key, secret.value);
+        }
+
+        if (valid) {
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
+            this.close(global.get_current_time());
+        }
+        // do nothing if not valid
+    },
+
+    cancel: function() {
+        this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
+        this.close(global.get_current_time());
+    },
+
+    _validateWpaPsk: function(secret) {
+        let value = secret.value;
+        if (value.length == 64) {
+            // must be composed of hexadecimal digits only
+            for (let i = 0; i < 64; i++) {
+                if (!((value[i] >= 'a' && value[i] <= 'f')
+                      || (value[i] >= 'A' && value[i] <= 'F')
+                      || (value[i] >= '0' && value[i] <= '9')))
+                    return false;
+            }
+            return true;
+        }
+
+        return (value.length >= 8 && value.length <= 63);
+    },
+
+    _validateStaticWep: function(secret) {
+        let value = secret.value;
+        if (secret.wep_key_type == NetworkManager.WepKeyType.KEY) {
+            if (value.length == 10 || value.length == 26) {
+               for (let i = 0; i < value.length; i++) {
+                    if (!((value[i] >= 'a' && value[i] <= 'f')
+                          || (value[i] >= 'A' && value[i] <= 'F')
+                          || (value[i] >= '0' && value[i] <= '9')))
+                        return false;
+               }
+           } else if (value.length == 5 || value.length == 13) {
+               for (let i = 0; i < value.length; i++) {
+                    if (!((value[i] >= 'a' && value[i] <= 'z')
+                          || (value[i] >= 'A' && value[i] <= 'Z')))
+                        return false;
+                }
+            } else
+                return false;
+       } else if (secret.wep_key_type == NetworkManager.WepKeyType.PASSPHRASE) {
+           if (value.length < 0 || value.length > 64)
+               return false;
+       }
+        return true;
+    },
+
+    _getWirelessSecrets: function(secrets, wirelessSetting) {
+        let wirelessSecuritySetting = this._connection.get_setting_wireless_security();
+        switch (wirelessSecuritySetting.key_mgmt) {
+        // First the easy ones
+        case 'wpa-none':
+        case 'wpa-psk':
+            secrets.push({ label: _("Password: "), key: 'psk',
+                           value: wirelessSecuritySetting.psk || '',
+                           validate: this._validateWpaPsk, password: true });
+            break;
+        case 'none': // static WEP
+            secrets.push({ label: _("Key: "), key: 'wep-key' + wirelessSecuritySetting.wep_tx_keyidx,
+                           value: wirelessSecuritySetting.get_wep_key(wirelessSecuritySetting.wep_tx_keyidx) || '',
+                           wep_key_type: wirelessSecuritySetting.wep_key_type,
+                           validate: this._validateStaticWep, password: true });
+            break;
+        case 'ieee8021x':
+            if (wirelessSecuritySetting.auth_alg == 'leap') // Cisco LEAP
+                secrets.push({ label: _("Password: "), key: 'leap-password',
+                               value: wirelessSecuritySetting.leap_password || '', password: true });
+            else // Dynamic (IEEE 802.1x) WEP
+                this._get8021xSecrets(secrets);
+            break;
+        case 'wpa-eap':
+            this._get8021xSecrets(secrets);
+            break;
+        default:
+            log('Invalid wireless key management: ' + wirelessSecuritySetting.key_mgmt);
+        }
+    },
+
+    _get8021xSecrets: function(secrets) {
+        let ieee8021xSetting = this._connection.get_setting_802_1x();
+        let phase2method;
+
+        switch (ieee8021xSetting.get_eap_method(0)) {
+        case 'md5':
+        case 'leap':
+        case 'ttls':
+        case 'peap':
+            // TTLS and PEAP are actually much more complicated, but this complication
+            // is not visible here since we only care about phase2 authentication
+            // (and don't even care of which one)
+            secrets.push({ label: _("Username: "), key: null,
+                           value: ieee8021xSetting.identity || '', password: false });
+            secrets.push({ label: _("Password: "), key: 'password',
+                           value: ieee8021xSetting.password || '', password: true });
+            break;
+        case 'tls':
+            secrets.push({ label: _("Identity: "), key: null,
+                           value: ieee8021xSetting.identity || '', password: false });
+            secrets.push({ label: _("Private key password: "), key: 'private-key-password',
+                           value: ieee8021xSetting.private_key_password || '', password: true });
+            break;
+        default:
+            log('Invalid EAP/IEEE802.1x method: ' + ieee8021xSetting.get_eap_method(0));
+        }
+    },
+
+    _getPPPoESecrets: function(secrets) {
+        let pppoeSetting = this._connection.get_setting_pppoe();
+        secrets.push({ label: _("Username: "), key: 'username',
+                       value: pppoeSetting.username || '', password: false });
+        secrets.push({ label: _("Service: "), key: 'service',
+                       value: pppoeSetting.service || '', password: false });
+        secrets.push({ label: _("Password: "), key: 'password',
+                       value: pppoeSetting.password || '', password: true });
+    },
+
+    _getMobileSecrets: function(secrets, connectionType) {
+        let setting;
+        if (connectionType == 'bluetooth')
+            setting = this._connection.get_setting_cdma() || this._connection.get_setting_gsm();
+        else
+            setting = this._connection.get_setting_by_name(connectionType);
+        secrets.push({ label: _("Password: "), key: 'password',
+                       value: setting.value || '', password: true });
+    },
+
+    _getContent: function() {
+        let connectionSetting = this._connection.get_setting_connection();
+        let connectionType = connectionSetting.get_connection_type();
+        let wirelessSetting;
+        let ssid;
+
+        let content = { };
+        content.secrets = [ ];
+
+        switch (connectionType) {
+        case '802-11-wireless':
+            wirelessSetting = this._connection.get_setting_wireless();
+            ssid = NetworkManager.utils_ssid_to_utf8(wirelessSetting.get_ssid());
+            content.title = _("Authentication required by wireless network");
+            content.message = _("Passwords or encryption keys are required to access the wireless network '%s'.").format(ssid);
+            this._getWirelessSecrets(content.secrets, wirelessSetting);
+            break;
+        case '802-3-ethernet':
+            content.title = _("Wired 802.1X authentication");
+            content.message = null;
+            content.secrets.push({ label: _("Network name: "), key: null,
+                                   value: connectionSetting.get_id(), password: false });
+            this._get8021xSecrets(content.secrets);
+            break;
+        case 'pppoe':
+            content.title = _("DSL authentication");
+            content.message = null;
+            this._getPPPoESecrets(content.secrets);
+            break;
+        case 'gsm':
+            if (this._hints.indexOf('pin') != -1) {
+                let gsmSetting = this._connection.get_setting_gsm();
+                content.title = _("PIN code required");
+                content.message = _("PIN code is needed for the mobile broadband device");
+                content.secrets.push({ label: _("PIN: "), key: 'pin',
+                                       value: gsmSetting.pin || '', password: true });
+            }
+            // fall through
+        case 'cdma':
+        case 'bluetooth':
+            content.title = _("Mobile broadband network password");
+            content.message = _("A password is required to connect to '%s'.").format(connectionSetting.get_id());
+            this._getMobileSecrets(content.secrets, connectionType);
+            break;
+        default:
+            log('Invalid connection type: ' + connectionType);
+        };
+
+        return content;
+    }
+});
+
+const VPNRequestHandler = new Lang.Class({
+    Name: 'VPNRequestHandler',
+
+    _init: function(agent, requestId, authHelper, serviceType, connection, hints, flags) {
+        this._agent = agent;
+        this._requestId = requestId;
+        this._connection = connection;
+        this._pluginOutBuffer = [];
+        this._title = null;
+        this._description = null;
+        this._content = [ ];
+        this._shellDialog = null;
+
+        let connectionSetting = connection.get_setting_connection();
+
+        let argv = [ authHelper.fileName,
+                     '-u', connectionSetting.uuid,
+                     '-n', connectionSetting.id,
+                     '-s', serviceType
+                   ];
+        if (authHelper.externalUIMode)
+            argv.push('--external-ui-mode');
+        if (flags & NMClient.SecretAgentGetSecretsFlags.ALLOW_INTERACTION)
+            argv.push('-i');
+        if (flags & NMClient.SecretAgentGetSecretsFlags.REQUEST_NEW)
+            argv.push('-r');
+
+        this._newStylePlugin = authHelper.externalUIMode;
+
+        try {
+            let [success, pid, stdin, stdout, stderr] =
+                GLib.spawn_async_with_pipes(null, /* pwd */
+                                            argv,
+                                            null, /* envp */
+                                            GLib.SpawnFlags.DO_NOT_REAP_CHILD,
+                                            null /* child_setup */);
+
+            this._childPid = pid;
+            this._stdin = new Gio.UnixOutputStream({ fd: stdin, close_fd: true });
+            this._stdout = new Gio.UnixInputStream({ fd: stdout, close_fd: true });
+            // We need this one too, even if don't actually care of what the process
+            // has to say on stderr, because otherwise the fd opened by g_spawn_async_with_pipes
+            // is kept open indefinitely
+            let stderrStream = new Gio.UnixInputStream({ fd: stderr, close_fd: true });
+            stderrStream.close(null);
+            this._dataStdout = new Gio.DataInputStream({ base_stream: this._stdout });
+
+            if (this._newStylePlugin)
+                this._readStdoutNewStyle();
+            else
+                this._readStdoutOldStyle();
+
+            this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid,
+                                                    Lang.bind(this, this._vpnChildFinished));
+
+            this._writeConnection();
+        } catch(e) {
+            logError(e, 'error while spawning VPN auth helper');
+
+            this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+        }
+    },
+
+    cancel: function() {
+        if (this._newStylePlugin && this._shellDialog) {
+            this._shellDialog.close(global.get_current_time());
+            this._shellDialog.destroy();
+        } else {
+            try {
+                this._stdin.write('QUIT\n\n', null);
+            } catch(e) { /* ignore broken pipe errors */ }
+        }
+
+        this.destroy();
+    },
+
+    destroy: function() {
+        if (this._destroyed)
+            return;
+
+        GLib.source_remove(this._childWatch);
+
+        this._stdin.close(null);
+        // Stdout is closed when we finish reading from it
+
+        this._destroyed = true;
+    },
+
+    _vpnChildFinished: function(pid, status, requestObj) {
+        if (this._newStylePlugin) {
+            // For new style plugin, all work is done in the async reading functions
+            // Just reap the process here
+            return;
+        }
+
+        let [exited, exitStatus] = Shell.util_wifexited(status);
+
+        if (exited) {
+            if (exitStatus != 0)
+                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
+            else
+                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
+        } else
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+
+        this.destroy();
+    },
+
+    _vpnChildProcessLineOldStyle: function(line) {
+        if (this._previousLine != undefined) {
+            // Two consecutive newlines mean that the child should be closed
+            // (the actual newlines are eaten by Gio.DataInputStream)
+            // Send a termination message
+            if (line == '' && this._previousLine == '') {
+                try {
+                    this._stdin.write('QUIT\n\n', null);
+                } catch(e) { /* ignore broken pipe errors */ }
+            } else {
+                this._agent.set_password(this._requestId, this._previousLine, line);
+                this._previousLine = undefined;
+            }
+        } else {
+            this._previousLine = line;
+        }
+    },
+
+    _readStdoutOldStyle: function() {
+        this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
+            let [line, len] = this._dataStdout.read_line_finish_utf8(result);
+
+            if (line == null) {
+                // end of file
+                this._stdout.close(null);
+                return;
+            }
+
+            this._vpnChildProcessLineOldStyle(line);
+
+            // try to read more!
+            this._readStdoutOldStyle();
+        }));
+    },
+
+    _readStdoutNewStyle: function() {
+        this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
+            let cnt = this._dataStdout.fill_finish(result);
+
+            if (cnt == 0) {
+                // end of file
+                this._showNewStyleDialog();
+
+                this._stdout.close(null);
+                return;
+            }
+
+            // Try to read more
+            this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size());
+            this._readStdoutNewStyle();
+        }));
+    },
+
+    _showNewStyleDialog: function() {
+        let keyfile = new GLib.KeyFile();
+        let contentOverride;
+
+        try {
+            let data = this._dataStdout.peek_buffer();
+            keyfile.load_from_data(data.toString(), data.length,
+                                   GLib.KeyFileFlags.NONE);
+
+            if (keyfile.get_integer(VPN_UI_GROUP, 'Version') != 2)
+                throw new Error('Invalid plugin keyfile version, is %d');
+
+            contentOverride = { title: keyfile.get_string(VPN_UI_GROUP, 'Title'),
+                                message: keyfile.get_string(VPN_UI_GROUP, 'Description'),
+                                secrets: [] };
+
+            let [groups, len] = keyfile.get_groups();
+            for (let i = 0; i < groups.length; i++) {
+                if (groups[i] == VPN_UI_GROUP)
+                    continue;
+
+                let value = keyfile.get_string(groups[i], 'Value');
+                let shouldAsk = keyfile.get_boolean(groups[i], 'ShouldAsk');
+
+                if (shouldAsk) {
+                    contentOverride.secrets.push({ label: keyfile.get_string(groups[i], 'Label'),
+                                                   key: groups[i],
+                                                   value: value,
+                                                   password: keyfile.get_boolean(groups[i], 'IsSecret')
+                                                 });
+                } else {
+                    if (!value.length) // Ignore empty secrets
+                        continue;
+
+                    this._agent.set_password(this._requestId, groups[i], value);
+                }
+            }
+        } catch(e) {
+            logError(e, 'error while reading VPN plugin output keyfile');
+
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+            return;
+        }
+
+        if (contentOverride.secrets.length) {
+            // Only show the dialog if we actually have something to ask
+            this._shellDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], contentOverride);
+            this._shellDialog.open(global.get_current_time());
+        } else {
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
+        }
+    },
+
+    _writeConnection: function() {
+        let vpnSetting = this._connection.get_setting_vpn();
+
+        try {
+            vpnSetting.foreach_data_item(Lang.bind(this, function(key, value) {
+                this._stdin.write('DATA_KEY=' + key + '\n', null);
+                this._stdin.write('DATA_VAL=' + (value || '') + '\n\n', null);
+            }));
+            vpnSetting.foreach_secret(Lang.bind(this, function(key, value) {
+                this._stdin.write('SECRET_KEY=' + key + '\n', null);
+                this._stdin.write('SECRET_VAL=' + (value || '') + '\n\n', null);
+            }));
+            this._stdin.write('DONE\n\n', null);
+        } catch(e) {
+            logError(e, 'internal error while writing connection to helper');
+
+            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+        }
+    },
+});
+
+const NetworkAgent = new Lang.Class({
+    Name: 'NetworkAgent',
+
+    _init: function() {
+        this._native = new Shell.NetworkAgent({ auto_register: false,
+                                                identifier: 'org.gnome.Shell.NetworkAgent' });
+
+        this._dialogs = { };
+        this._vpnRequests = { };
+
+        this._native.connect('new-request', Lang.bind(this, this._newRequest));
+        this._native.connect('cancel-request', Lang.bind(this, this._cancelRequest));
+    },
+
+    enable: function() {
+        this._native.register();
+    },
+
+    disable: function() {
+        this._native.unregister();
+    },
+
+    _newRequest:  function(agent, requestId, connection, settingName, hints, flags) {
+        if (settingName == 'vpn') {
+            this._vpnRequest(requestId, connection, hints, flags);
+            return;
+        }
+
+        let dialog = new NetworkSecretDialog(agent, requestId, connection, settingName, hints);
+        dialog.connect('destroy', Lang.bind(this, function() {
+            delete this._dialogs[requestId];
+        }));
+        this._dialogs[requestId] = dialog;
+        dialog.open(global.get_current_time());
+    },
+
+    _cancelRequest: function(agent, requestId) {
+        if (this._dialogs[requestId]) {
+            this._dialogs[requestId].close(global.get_current_time());
+            this._dialogs[requestId].destroy();
+            delete this._dialogs[requestId];
+        } else if (this._vpnRequests[requestId]) {
+            this._vpnRequests[requestId].cancel();
+            delete this._vpnRequests[requestId];
+        }
+    },
+
+    _vpnRequest: function(requestId, connection, hints, flags) {
+        let vpnSetting = connection.get_setting_vpn();
+        let serviceType = vpnSetting.service_type;
+
+        this._buildVPNServiceCache();
+
+        let binary = this._vpnBinaries[serviceType];
+        if (!binary) {
+            log('Invalid VPN service type (cannot find authentication binary)');
+
+            /* cancel the auth process */
+            this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
+            return;
+        }
+
+        this._vpnRequests[requestId] = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags);
+    },
+
+    _buildVPNServiceCache: function() {
+        if (this._vpnCacheBuilt)
+            return;
+
+        this._vpnCacheBuilt = true;
+        this._vpnBinaries = { };
+
+        let dir = Gio.file_new_for_path(GLib.build_filenamev([Config.SYSCONFDIR, 'NetworkManager/VPN']));
+        try {
+            let fileEnum = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
+            let info;
+
+            while ((info = fileEnum.next_file(null))) {
+                let name = info.get_name();
+                if (name.substr(-5) != '.name')
+                    continue;
+
+                try {
+                    let keyfile = new GLib.KeyFile();
+                    keyfile.load_from_file(dir.get_child(name).get_path(), GLib.KeyFileFlags.NONE);
+                    let service = keyfile.get_string('VPN Connection', 'service');
+                    let binary = keyfile.get_string('GNOME', 'auth-dialog');
+                    let externalUIMode = false;
+                    try {
+                        externalUIMode = keyfile.get_boolean('GNOME', 'supports-external-ui-mode');
+                    } catch(e) { } // ignore errors if key does not exist
+                    let path = binary;
+                    if (!GLib.path_is_absolute(path)) {
+                        path = GLib.build_filenamev([Config.LIBEXECDIR, path]);
+                    }
+
+                    if (GLib.file_test(path, GLib.FileTest.IS_EXECUTABLE))
+                        this._vpnBinaries[service] = { fileName: path, externalUIMode: externalUIMode };
+                    else
+                        throw new Error('VPN plugin at %s is not executable'.format(path));
+                } catch(e) {
+                    log('Error \'%s\' while processing VPN keyfile \'%s\''.
+                        format(e.message, dir.get_child(name).get_path()));
+                    continue;
+                }
+            }
+        } catch(e) {
+            logError(e, 'error while enumerating VPN auth helpers');
+        }
+    }
+});
+const Component = NetworkAgent;
diff --git a/js/ui/components/polkitAgent.js b/js/ui/components/polkitAgent.js
new file mode 100644 (file)
index 0000000..0fa9ba4
--- /dev/null
@@ -0,0 +1,391 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Lang = imports.lang;
+const Signals = imports.signals;
+const Shell = imports.gi.Shell;
+const AccountsService = imports.gi.AccountsService;
+const Clutter = imports.gi.Clutter;
+const St = imports.gi.St;
+const Pango = imports.gi.Pango;
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Mainloop = imports.mainloop;
+const Polkit = imports.gi.Polkit;
+const PolkitAgent = imports.gi.PolkitAgent;
+
+const Components = imports.ui.components;
+const ModalDialog = imports.ui.modalDialog;
+const ShellEntry = imports.ui.shellEntry;
+const UserMenu = imports.ui.userMenu;
+
+const DIALOG_ICON_SIZE = 48;
+
+const AuthenticationDialog = new Lang.Class({
+    Name: 'AuthenticationDialog',
+    Extends: ModalDialog.ModalDialog,
+
+    _init: function(actionId, message, cookie, userNames) {
+        this.parent({ styleClass: 'prompt-dialog' });
+
+        this.actionId = actionId;
+        this.message = message;
+        this.userNames = userNames;
+        this._wasDismissed = false;
+        this._completed = false;
+
+        let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
+                                                vertical: false });
+        this.contentLayout.add(mainContentBox,
+                               { x_fill: true,
+                                 y_fill: true });
+
+        let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
+        mainContentBox.add(icon,
+                           { x_fill:  true,
+                             y_fill:  false,
+                             x_align: St.Align.END,
+                             y_align: St.Align.START });
+
+        let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
+                                            vertical: true });
+        mainContentBox.add(messageBox,
+                           { y_align: St.Align.START });
+
+        this._subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
+                                            text: _("Authentication Required") });
+
+        messageBox.add(this._subjectLabel,
+                       { y_fill:  false,
+                         y_align: St.Align.START });
+
+        this._descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
+                                                text: message });
+        this._descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this._descriptionLabel.clutter_text.line_wrap = true;
+
+        messageBox.add(this._descriptionLabel,
+                       { y_fill:  true,
+                         y_align: St.Align.START });
+
+        if (userNames.length > 1) {
+            log('polkitAuthenticationAgent: Received ' + userNames.length +
+                ' identities that can be used for authentication. Only ' +
+                'considering one.');
+        }
+
+        let userName = GLib.get_user_name();
+        if (userNames.indexOf(userName) < 0)
+            userName = 'root';
+        if (userNames.indexOf(userName) < 0)
+            userName = userNames[0];
+
+        this._user = AccountsService.UserManager.get_default().get_user(userName);
+        let userRealName = this._user.get_real_name()
+        this._userLoadedId = this._user.connect('notify::is_loaded',
+                                                Lang.bind(this, this._onUserChanged));
+        this._userChangedId = this._user.connect('changed',
+                                                 Lang.bind(this, this._onUserChanged));
+
+        // Special case 'root'
+        let userIsRoot = false;
+        if (userName == 'root') {
+            userIsRoot = true;
+            userRealName = _("Administrator");
+        }
+
+        if (userIsRoot) {
+            let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-root-label',
+                                            text: userRealName }));
+            messageBox.add(userLabel);
+        } else {
+            let userBox = new St.BoxLayout({ style_class: 'polkit-dialog-user-layout',
+                                             vertical: false });
+            messageBox.add(userBox);
+            this._userAvatar = new UserMenu.UserAvatarWidget(this._user,
+                                                             { iconSize: DIALOG_ICON_SIZE,
+                                                               styleClass: 'polkit-dialog-user-icon' });
+            this._userAvatar.actor.hide();
+            userBox.add(this._userAvatar.actor,
+                        { x_fill:  true,
+                          y_fill:  false,
+                          x_align: St.Align.END,
+                          y_align: St.Align.START });
+            let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-label',
+                                            text: userRealName }));
+            userBox.add(userLabel,
+                        { x_fill:  true,
+                          y_fill:  false,
+                          x_align: St.Align.END,
+                          y_align: St.Align.MIDDLE });
+        }
+
+        this._onUserChanged();
+
+        this._passwordBox = new St.BoxLayout({ vertical: false, style_class: 'prompt-dialog-password-box' });
+        messageBox.add(this._passwordBox);
+        this._passwordLabel = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
+        this._passwordBox.add(this._passwordLabel, { y_fill: false, y_align: St.Align.MIDDLE });
+        this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
+                                             text: "",
+                                             can_focus: true});
+        ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
+        this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivate));
+        this._passwordBox.add(this._passwordEntry,
+                              { expand: true });
+        this.setInitialKeyFocus(this._passwordEntry);
+        this._passwordBox.hide();
+
+        this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label' });
+        this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this._errorMessageLabel.clutter_text.line_wrap = true;
+        messageBox.add(this._errorMessageLabel);
+        this._errorMessageLabel.hide();
+
+        this._infoMessageLabel = new St.Label({ style_class: 'prompt-dialog-info-label' });
+        this._infoMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this._infoMessageLabel.clutter_text.line_wrap = true;
+        messageBox.add(this._infoMessageLabel);
+        this._infoMessageLabel.hide();
+
+        /* text is intentionally non-blank otherwise the height is not the same as for
+         * infoMessage and errorMessageLabel - but it is still invisible because
+         * gnome-shell.css sets the color to be transparent
+         */
+        this._nullMessageLabel = new St.Label({ style_class: 'prompt-dialog-null-label',
+                                                text: 'abc'});
+        this._nullMessageLabel.add_style_class_name('hidden');
+        this._nullMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this._nullMessageLabel.clutter_text.line_wrap = true;
+        messageBox.add(this._nullMessageLabel);
+        this._nullMessageLabel.show();
+
+        this.setButtons([{ label: _("Cancel"),
+                           action: Lang.bind(this, this.cancel),
+                           key:    Clutter.Escape
+                         },
+                         { label:  _("Authenticate"),
+                           action: Lang.bind(this, this._onAuthenticateButtonPressed),
+                           default: true
+                         }]);
+
+        this._doneEmitted = false;
+
+        this._identityToAuth = Polkit.UnixUser.new_for_name(userName);
+        this._cookie = cookie;
+
+        this._session = new PolkitAgent.Session({ identity: this._identityToAuth,
+                                                  cookie: this._cookie });
+        this._session.connect('completed', Lang.bind(this, this._onSessionCompleted));
+        this._session.connect('request', Lang.bind(this, this._onSessionRequest));
+        this._session.connect('show-error', Lang.bind(this, this._onSessionShowError));
+        this._session.connect('show-info', Lang.bind(this, this._onSessionShowInfo));
+    },
+
+    startAuthentication: function() {
+        this._session.initiate();
+    },
+
+    _ensureOpen: function() {
+        // NOTE: ModalDialog.open() is safe to call if the dialog is
+        // already open - it just returns true without side-effects
+        if (!this.open(global.get_current_time())) {
+            // This can fail if e.g. unable to get input grab
+            //
+            // In an ideal world this wouldn't happen (because the
+            // Shell is in complete control of the session) but that's
+            // just not how things work right now.
+            //
+            // One way to make this happen is by running 'sleep 3;
+            // pkexec bash' and then opening a popup menu.
+            //
+            // We could add retrying if this turns out to be a problem
+
+            log('polkitAuthenticationAgent: Failed to show modal dialog.' +
+                ' Dismissing authentication request for action-id ' + this.actionId +
+                ' cookie ' + this._cookie);
+            this._emitDone(false, true);
+        }
+    },
+
+    _emitDone: function(keepVisible, dismissed) {
+        if (!this._doneEmitted) {
+            this._doneEmitted = true;
+            this.emit('done', keepVisible, dismissed);
+        }
+    },
+
+    _onEntryActivate: function() {
+        let response = this._passwordEntry.get_text();
+        this._session.response(response);
+        // When the user responds, dismiss already shown info and
+        // error texts (if any)
+        this._errorMessageLabel.hide();
+        this._infoMessageLabel.hide();
+        this._nullMessageLabel.show();
+    },
+
+    _onAuthenticateButtonPressed: function() {
+        this._onEntryActivate();
+    },
+
+    _onSessionCompleted: function(session, gainedAuthorization) {
+        if (this._completed)
+            return;
+
+        this._completed = true;
+
+        if (!gainedAuthorization) {
+            /* Unless we are showing an existing error message from the PAM
+             * module (the PAM module could be reporting the authentication
+             * error providing authentication-method specific information),
+             * show "Sorry, that didn't work. Please try again."
+             */
+            if (!this._errorMessageLabel.visible && !this._wasDismissed) {
+                /* Translators: "that didn't work" refers to the fact that the
+                 * requested authentication was not gained; this can happen
+                 * because of an authentication error (like invalid password),
+                 * for instance. */
+                this._errorMessageLabel.set_text(_("Sorry, that didn\'t work. Please try again."));
+                this._errorMessageLabel.show();
+                this._infoMessageLabel.hide();
+                this._nullMessageLabel.hide();
+            }
+        }
+        this._emitDone(!gainedAuthorization, false);
+    },
+
+    _onSessionRequest: function(session, request, echo_on) {
+        // Cheap localization trick
+        if (request == 'Password:' || request == 'Password: ')
+            this._passwordLabel.set_text(_("Password:"));
+        else
+            this._passwordLabel.set_text(request);
+
+        if (echo_on)
+            this._passwordEntry.clutter_text.set_password_char('');
+        else
+            this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
+
+        this._passwordBox.show();
+        this._passwordEntry.set_text('');
+        this._passwordEntry.grab_key_focus();
+        this._ensureOpen();
+    },
+
+    _onSessionShowError: function(session, text) {
+        this._passwordEntry.set_text('');
+        this._errorMessageLabel.set_text(text);
+        this._errorMessageLabel.show();
+        this._infoMessageLabel.hide();
+        this._nullMessageLabel.hide();
+        this._ensureOpen();
+    },
+
+    _onSessionShowInfo: function(session, text) {
+        this._passwordEntry.set_text('');
+        this._infoMessageLabel.set_text(text);
+        this._infoMessageLabel.show();
+        this._errorMessageLabel.hide();
+        this._nullMessageLabel.hide();
+        this._ensureOpen();
+    },
+
+    destroySession: function() {
+        if (this._session) {
+            if (!this._completed)
+                this._session.cancel();
+            this._session = null;
+        }
+    },
+
+    _onUserChanged: function() {
+        if (this._user.is_loaded) {
+            this._userAvatar.update();
+            this._userAvatar.actor.show();
+        }
+    },
+
+    cancel: function() {
+        this._wasDismissed = true;
+        this.close(global.get_current_time());
+        this._emitDone(false, true);
+    },
+});
+Signals.addSignalMethods(AuthenticationDialog.prototype);
+
+const AuthenticationAgent = new Lang.Class({
+    Name: 'AuthenticationAgent',
+
+    _init: function() {
+        this._currentDialog = null;
+        this._isCompleting = false;
+        this._handle = null;
+        this._native = new Shell.PolkitAuthenticationAgent();
+        this._native.connect('initiate', Lang.bind(this, this._onInitiate));
+        this._native.connect('cancel', Lang.bind(this, this._onCancel));
+    },
+
+    enable: function() {
+        this._native.register();
+    },
+
+    disable: function() {
+        this._native.unregister();
+    },
+
+    _onInitiate: function(nativeAgent, actionId, message, iconName, cookie, userNames) {
+        this._currentDialog = new AuthenticationDialog(actionId, message, cookie, userNames);
+
+        // We actually don't want to open the dialog until we know for
+        // sure that we're going to interact with the user. For
+        // example, if the password for the identity to auth is blank
+        // (which it will be on a live CD) then there will be no
+        // conversation at all... of course, we don't *know* that
+        // until we actually try it.
+        //
+        // See https://bugzilla.gnome.org/show_bug.cgi?id=643062 for more
+        // discussion.
+
+        this._currentDialog.connect('done', Lang.bind(this, this._onDialogDone));
+        this._currentDialog.startAuthentication();
+    },
+
+    _onCancel: function(nativeAgent) {
+        this._completeRequest(false, false);
+    },
+
+    _onDialogDone: function(dialog, keepVisible, dismissed) {
+        this._completeRequest(keepVisible, dismissed);
+    },
+
+    _reallyCompleteRequest: function(dismissed) {
+        this._currentDialog.close();
+        this._currentDialog.destroySession();
+        this._currentDialog = null;
+        this._isCompleting = false;
+
+        this._native.complete(dismissed)
+    },
+
+    _completeRequest: function(keepVisible, wasDismissed) {
+        if (this._isCompleting)
+            return;
+
+        this._isCompleting = true;
+
+        if (keepVisible) {
+            // Give the user 2 seconds to read 'Authentication Failure' before
+            // dismissing the dialog
+            Mainloop.timeout_add(2000,
+                                 Lang.bind(this,
+                                           function() {
+                                               this._reallyCompleteRequest(wasDismissed);
+                                               return false;
+                                           }));
+        } else {
+            this._reallyCompleteRequest(wasDismissed);
+        }
+    }
+});
+
+const Component = AuthenticationAgent;
diff --git a/js/ui/components/recorder.js b/js/ui/components/recorder.js
new file mode 100644 (file)
index 0000000..94c49ce
--- /dev/null
@@ -0,0 +1,58 @@
+
+const Lang = imports.lang;
+
+const Gio = imports.gi.Gio;
+const Meta = imports.gi.Meta;
+const Shell = imports.gi.Shell;
+
+const Recorder = new Lang.Class({
+    Name: 'Recorder',
+
+    _init: function() {
+        this._recorderSettings = new Gio.Settings({ schema: 'org.gnome.shell.recorder' });
+        this._desktopLockdownSettings = new Gio.Settings({ schema: 'org.gnome.desktop.lockdown' });
+        this._bindingSettings = new Gio.Settings({ schema: 'org.gnome.shell.keybindings' });
+        this._recorder = null;
+    },
+
+    enable: function() {
+        global.display.add_keybinding('toggle-recording',
+                                      this._bindingSettings,
+                                      Meta.KeyBindingFlags.NONE, Lang.bind(this, this._toggleRecorder));
+    },
+
+    disable: function() {
+        global.display.remove_keybinding('toggle-recording');
+    },
+
+    _ensureRecorder: function() {
+        if (this._recorder == null)
+            this._recorder = new Shell.Recorder({ stage: global.stage });
+        return this._recorder;
+    },
+
+    _toggleRecorder: function() {
+        let recorder = this._ensureRecorder();
+        if (recorder.is_recording()) {
+            recorder.close();
+            Meta.enable_unredirect_for_screen(global.screen);
+        } else if (!desktopLockdownSettings.get_boolean('disable-save-to-disk')) {
+            // read the parameters from GSettings always in case they have changed
+            recorder.set_framerate(recorderSettings.get_int('framerate'));
+            /* Translators: this is a filename used for screencast recording */
+            // xgettext:no-c-format
+            recorder.set_filename(_("Screencast from %d %t") + '.' + recorderSettings.get_string('file-extension'));
+            let pipeline = recorderSettings.get_string('pipeline');
+
+            if (!pipeline.match(/^\s*$/))
+                recorder.set_pipeline(pipeline);
+            else
+                recorder.set_pipeline(null);
+
+            Meta.disable_unredirect_for_screen(global.screen);
+            recorder.record();
+        }
+    }
+});
+
+const Component = Recorder;
diff --git a/js/ui/components/telepathyClient.js b/js/ui/components/telepathyClient.js
new file mode 100644 (file)
index 0000000..a6abeb1
--- /dev/null
@@ -0,0 +1,1411 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+const St = imports.gi.St;
+const Tpl = imports.gi.TelepathyLogger;
+const Tp = imports.gi.TelepathyGLib;
+
+const History = imports.misc.history;
+const Params = imports.misc.params;
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+
+
+// See Notification.appendMessage
+const SCROLLBACK_IMMEDIATE_TIME = 60; // 1 minute
+const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
+const SCROLLBACK_RECENT_LENGTH = 20;
+const SCROLLBACK_IDLE_LENGTH = 5;
+
+// See Source._displayPendingMessages
+const SCROLLBACK_HISTORY_LINES = 10;
+
+// See Notification._onEntryChanged
+const COMPOSING_STOP_TIMEOUT = 5;
+
+const NotificationDirection = {
+    SENT: 'chat-sent',
+    RECEIVED: 'chat-received'
+};
+
+function makeMessageFromTpMessage(tpMessage, direction) {
+    let [text, flags] = tpMessage.to_text();
+
+    let timestamp = tpMessage.get_sent_timestamp();
+    if (timestamp == 0)
+        timestamp = tpMessage.get_received_timestamp();
+
+    return {
+        messageType: tpMessage.get_message_type(),
+        text: text,
+        sender: tpMessage.sender.alias,
+        timestamp: timestamp,
+        direction: direction
+    };
+}
+
+
+function makeMessageFromTplEvent(event) {
+    let sent = event.get_sender().get_entity_type() == Tpl.EntityType.SELF;
+    let direction = sent ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
+
+    return {
+        messageType: event.get_message_type(),
+        text: event.get_message(),
+        sender: event.get_sender().get_alias(),
+        timestamp: event.get_timestamp(),
+        direction: direction
+    };
+}
+
+const TelepathyClient = new Lang.Class({
+    Name: 'TelepathyClient',
+
+    _init: function() {
+        // channel path -> ChatSource
+        this._chatSources = {};
+        this._chatState = Tp.ChannelChatState.ACTIVE;
+
+        // account path -> AccountNotification
+        this._accountNotifications = {};
+
+        // Define features we want
+        this._accountManager = Tp.AccountManager.dup();
+        let factory = this._accountManager.get_factory();
+        factory.add_account_features([Tp.Account.get_feature_quark_connection()]);
+        factory.add_connection_features([Tp.Connection.get_feature_quark_contact_list()]);
+        factory.add_channel_features([Tp.Channel.get_feature_quark_contacts()]);
+        factory.add_contact_features([Tp.ContactFeature.ALIAS,
+                                      Tp.ContactFeature.AVATAR_DATA,
+                                      Tp.ContactFeature.PRESENCE,
+                                      Tp.ContactFeature.SUBSCRIPTION_STATES]);
+
+        // Set up a SimpleObserver, which will call _observeChannels whenever a
+        // channel matching its filters is detected.
+        // The second argument, recover, means _observeChannels will be run
+        // for any existing channel as well.
+        this._tpClient = new Shell.TpClient({ name: 'GnomeShell',
+                                              account_manager: this._accountManager,
+                                              uniquify_name: true });
+        this._tpClient.set_observe_channels_func(
+            Lang.bind(this, this._observeChannels));
+        this._tpClient.set_approve_channels_func(
+            Lang.bind(this, this._approveChannels));
+        this._tpClient.set_handle_channels_func(
+            Lang.bind(this, this._handleChannels));
+
+        // Watch subscription requests and connection errors
+        this._subscriptionSource = null;
+        this._accountSource = null;
+
+        // Workaround for gjs not supporting GPtrArray in signals.
+        // See BGO bug #653941 for context.
+        this._tpClient.set_contact_list_changed_func(
+            Lang.bind(this, this._contactListChanged));
+
+        // Allow other clients (such as Empathy) to pre-empt our channels if
+        // needed
+        this._tpClient.set_delegated_channels_callback(
+            Lang.bind(this, this._delegatedChannelsCb));
+    },
+
+    enable: function() {
+        try {
+            this._tpClient.register();
+        } catch (e) {
+            throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
+        }
+
+        this._accountManagerValidityChangedId = this._accountManager.connect('account-validity-changed',
+                                                                             Lang.bind(this, this._accountValidityChanged));
+
+        if (!this._accountManager.is_prepared(Tp.AccountManager.get_feature_quark_core()))
+            this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared));
+    },
+
+    disable: function() {
+        this._tpClient.unregister();
+        this._accountManager.disconnect(this._accountManagerValidityChangedId);
+        this._accountManagerValidityChangedId = 0;
+    },
+
+    _observeChannels: function(observer, account, conn, channels,
+                               dispatchOp, requests, context) {
+        let len = channels.length;
+        for (let i = 0; i < len; i++) {
+            let channel = channels[i];
+            let [targetHandle, targetHandleType] = channel.get_handle();
+
+            if (channel.get_invalidated())
+              continue;
+
+            /* Only observe contact text channels */
+            if ((!(channel instanceof Tp.TextChannel)) ||
+               targetHandleType != Tp.HandleType.CONTACT)
+               continue;
+
+            this._createChatSource(account, conn, channel, channel.get_target_contact());
+        }
+
+        context.accept();
+    },
+
+    _createChatSource: function(account, conn, channel, contact) {
+        if (this._chatSources[channel.get_object_path()])
+            return;
+
+        let source = new ChatSource(account, conn, channel, contact, this._tpClient);
+
+        this._chatSources[channel.get_object_path()] = source;
+        source.connect('destroy', Lang.bind(this,
+                       function() {
+                           if (this._tpClient.is_handling_channel(channel)) {
+                               // The chat box has been destroyed so it can't
+                               // handle the channel any more.
+                               channel.close_async(function(src, result) {
+                                   channel.close_finish(result);
+                               });
+                           }
+
+                           delete this._chatSources[channel.get_object_path()];
+                       }));
+    },
+
+    _handleChannels: function(handler, account, conn, channels,
+                              requests, user_action_time, context) {
+        this._handlingChannels(account, conn, channels, true);
+        context.accept();
+    },
+
+    _handlingChannels: function(account, conn, channels, notify) {
+        let len = channels.length;
+        for (let i = 0; i < len; i++) {
+            let channel = channels[i];
+
+            // We can only handle text channel, so close any other channel
+            if (!(channel instanceof Tp.TextChannel)) {
+                channel.close_async(null);
+                continue;
+            }
+
+            if (channel.get_invalidated())
+              continue;
+
+            // 'notify' will be true when coming from an actual HandleChannels
+            // call, and not when from a successful Claim call. The point is
+            // we don't want to notify for a channel we just claimed which
+            // has no new messages (for example, a new channel which only has
+            // a delivery notification). We rely on _displayPendingMessages()
+            // and _messageReceived() to notify for new messages.
+
+            // But we should still notify from HandleChannels because the
+            // Telepathy spec states that handlers must foreground channels
+            // in HandleChannels calls which are already being handled.
+
+            if (notify && this._tpClient.is_handling_channel(channel)) {
+                // We are already handling the channel, display the source
+                let source = this._chatSources[channel.get_object_path()];
+                if (source)
+                    source.notify();
+            }
+        }
+    },
+
+    _displayRoomInvitation: function(conn, channel, dispatchOp, context) {
+        // We can only approve the rooms if we have been invited to it
+        let selfContact = channel.group_get_self_contact();
+        if (selfContact == null) {
+            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
+                                        message: 'Not invited to the room' }));
+            return;
+        }
+
+        let [invited, inviter, reason, msg] = channel.group_get_local_pending_contact_info(selfContact);
+        if (!invited) {
+            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
+                                        message: 'Not invited to the room' }));
+            return;
+        }
+
+        // FIXME: We don't have a 'chat room' icon (bgo #653737) use
+        // system-users for now as Empathy does.
+        let source = new ApproverSource(dispatchOp, _("Invitation"),
+                                        Gio.icon_new_for_string('system-users'));
+        Main.messageTray.add(source);
+
+        let notif = new RoomInviteNotification(source, dispatchOp, channel, inviter);
+        source.notify(notif);
+        context.accept();
+    },
+
+    _approveChannels: function(approver, account, conn, channels,
+                               dispatchOp, context) {
+        let channel = channels[0];
+        let chanType = channel.get_channel_type();
+
+        if (channel.get_invalidated()) {
+            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
+                                        message: 'Channel is invalidated' }));
+            return;
+        }
+
+        if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT)
+            this._approveTextChannel(account, conn, channel, dispatchOp, context);
+        else if (chanType == Tp.IFACE_CHANNEL_TYPE_CALL)
+            this._approveCall(account, conn, channel, dispatchOp, context);
+        else if (chanType == Tp.IFACE_CHANNEL_TYPE_FILE_TRANSFER)
+            this._approveFileTransfer(account, conn, channel, dispatchOp, context);
+        else
+            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
+                                        message: 'Unsupported channel type' }));
+    },
+
+    _approveTextChannel: function(account, conn, channel, dispatchOp, context) {
+        let [targetHandle, targetHandleType] = channel.get_handle();
+
+        if (targetHandleType == Tp.HandleType.CONTACT) {
+            // Approve private text channels right away as we are going to handle it
+            dispatchOp.claim_with_async(this._tpClient,
+                                        Lang.bind(this, function(dispatchOp, result) {
+                try {
+                    dispatchOp.claim_with_finish(result);
+                    this._handlingChannels(account, conn, [channel], false);
+                } catch (err) {
+                    throw new Error('Failed to Claim channel: ' + err);
+                }}));
+
+            context.accept();
+        } else {
+            this._displayRoomInvitation(conn, channel, dispatchOp, context);
+        }
+    },
+
+    _approveCall: function(account, conn, channel, dispatchOp, context) {
+        let isVideo = false;
+
+        let props = channel.borrow_immutable_properties();
+
+        if (props[Tp.PROP_CHANNEL_TYPE_CALL_INITIAL_VIDEO])
+          isVideo = true;
+
+        // We got the TpContact
+        let source = new ApproverSource(dispatchOp, _("Call"), isVideo ?
+                                        Gio.icon_new_for_string('camera-web') :
+                                        Gio.icon_new_for_string('audio-input-microphone'));
+        Main.messageTray.add(source);
+
+        let notif = new AudioVideoNotification(source, dispatchOp, channel,
+            channel.get_target_contact(), isVideo);
+        source.notify(notif);
+        context.accept();
+    },
+
+    _approveFileTransfer: function(account, conn, channel, dispatchOp, context) {
+        // Use the icon of the file being transferred
+        let gicon = Gio.content_type_get_icon(channel.get_mime_type());
+
+        // We got the TpContact
+        let source = new ApproverSource(dispatchOp, _("File Transfer"), gicon);
+        Main.messageTray.add(source);
+
+        let notif = new FileTransferNotification(source, dispatchOp, channel,
+            channel.get_target_contact());
+        source.notify(notif);
+        context.accept();
+    },
+
+    _delegatedChannelsCb: function(client, channels) {
+        // Nothing to do as we don't make a distinction between observed and
+        // handled channels.
+    },
+
+    _accountManagerPrepared: function(am, result) {
+        am.prepare_finish(result);
+
+        let accounts = am.get_valid_accounts();
+        for (let i = 0; i < accounts.length; i++) {
+            this._accountValidityChanged(am, accounts[i], true);
+        }
+    },
+
+    _accountValidityChanged: function(am, account, valid) {
+        if (!valid)
+            return;
+
+        // It would be better to connect to "status-changed" but we cannot.
+        // See discussion in https://bugzilla.gnome.org/show_bug.cgi?id=654159
+        account.connect("notify::connection-status",
+                        Lang.bind(this, this._accountConnectionStatusNotifyCb));
+
+        account.connect('notify::connection',
+                        Lang.bind(this, this._connectionChanged));
+        this._connectionChanged(account);
+    },
+
+    _connectionChanged: function(account) {
+        let conn = account.get_connection();
+        if (conn == null)
+            return;
+
+        this._tpClient.grab_contact_list_changed(conn);
+        if (conn.get_contact_list_state() == Tp.ContactListState.SUCCESS) {
+            this._contactListChanged(conn, conn.dup_contact_list(), []);
+        }
+    },
+
+    _contactListChanged: function(conn, added, removed) {
+        for (let i = 0; i < added.length; i++) {
+            let contact = added[i];
+
+            contact.connect('subscription-states-changed',
+                            Lang.bind(this, this._subscriptionStateChanged));
+            this._subscriptionStateChanged(contact);
+        }
+    },
+
+    _subscriptionStateChanged: function(contact) {
+        if (contact.get_publish_state() != Tp.SubscriptionState.ASK)
+            return;
+
+        /* Implicitly accept publish requests if contact is already subscribed */
+        if (contact.get_subscribe_state() == Tp.SubscriptionState.YES ||
+            contact.get_subscribe_state() == Tp.SubscriptionState.ASK) {
+
+            contact.authorize_publication_async(function(src, result) {
+                src.authorize_publication_finish(result)});
+
+            return;
+        }
+
+        /* Display notification to ask user to accept/reject request */
+        let source = this._ensureSubscriptionSource();
+
+        let notif = new SubscriptionRequestNotification(source, contact);
+        source.notify(notif);
+    },
+
+    _ensureSubscriptionSource: function() {
+        if (this._subscriptionSource == null) {
+            this._subscriptionSource = new MessageTray.Source(_("Subscription request"),
+                                                              'gtk-dialog-question');
+            Main.messageTray.add(this._subscriptionSource);
+            this._subscriptionSource.connect('destroy', Lang.bind(this, function () {
+                this._subscriptionSource = null;
+            }));
+        }
+
+        return this._subscriptionSource;
+    },
+
+    _accountConnectionStatusNotifyCb: function(account) {
+        let connectionError = account.connection_error;
+
+        if (account.connection_status != Tp.ConnectionStatus.DISCONNECTED ||
+            connectionError == Tp.error_get_dbus_name(Tp.Error.CANCELLED)) {
+            return;
+        }
+
+        let notif = this._accountNotifications[account.get_object_path()];
+        if (notif)
+            return;
+
+        /* Display notification that account failed to connect */
+        let source = this._ensureAccountSource();
+
+        notif = new AccountNotification(source, account, connectionError);
+        this._accountNotifications[account.get_object_path()] = notif;
+        notif.connect('destroy', Lang.bind(this, function() {
+            delete this._accountNotifications[account.get_object_path()];
+        }));
+        source.notify(notif);
+    },
+
+    _ensureAccountSource: function() {
+        if (this._accountSource == null) {
+            this._accountSource = new MessageTray.Source(_("Connection error"),
+                                                         'gtk-dialog-error');
+            Main.messageTray.add(this._accountSource);
+            this._accountSource.connect('destroy', Lang.bind(this, function () {
+                this._accountSource = null;
+            }));
+        }
+
+        return this._accountSource;
+    }
+});
+
+const ChatSource = new Lang.Class({
+    Name: 'ChatSource',
+    Extends: MessageTray.Source,
+
+    _init: function(account, conn, channel, contact, client) {
+        this._account = account;
+        this._contact = contact;
+        this._client = client;
+
+        this.parent(contact.get_alias());
+
+        this.isChat = true;
+        this._pendingMessages = [];
+
+        this._conn = conn;
+        this._channel = channel;
+        this._closedId = this._channel.connect('invalidated', Lang.bind(this, this._channelClosed));
+
+        this._notification = new ChatNotification(this);
+        this._notification.setUrgency(MessageTray.Urgency.HIGH);
+        this._notifyTimeoutId = 0;
+
+        // We ack messages when the user expands the new notification or views the summary
+        // notification, in which case the notification is also expanded.
+        this._notification.connect('expanded', Lang.bind(this, this._ackMessages));
+
+        this._presence = contact.get_presence_type();
+
+        this._sentId = this._channel.connect('message-sent', Lang.bind(this, this._messageSent));
+        this._receivedId = this._channel.connect('message-received', Lang.bind(this, this._messageReceived));
+        this._pendingId = this._channel.connect('pending-message-removed', Lang.bind(this, this._pendingRemoved));
+
+        this._notifyAliasId = this._contact.connect('notify::alias', Lang.bind(this, this._updateAlias));
+        this._notifyAvatarId = this._contact.connect('notify::avatar-file', Lang.bind(this, this._updateAvatarIcon));
+        this._presenceChangedId = this._contact.connect('presence-changed', Lang.bind(this, this._presenceChanged));
+
+        // Add ourselves as a source.
+        Main.messageTray.add(this);
+        this.pushNotification(this._notification);
+
+        this._getLogMessages();
+    },
+
+    _updateAlias: function() {
+        let oldAlias = this.title;
+        let newAlias = this._contact.get_alias();
+
+        if (oldAlias == newAlias)
+            return;
+
+        this.setTitle(newAlias);
+        this._notification.appendAliasChange(oldAlias, newAlias);
+    },
+
+    createIcon: function(size) {
+        this._iconBox = new St.Bin({ style_class: 'avatar-box' });
+        this._iconBox._size = size;
+        let textureCache = St.TextureCache.get_default();
+        let file = this._contact.get_avatar_file();
+
+        if (file) {
+            let uri = file.get_uri();
+            this._iconBox.child = textureCache.load_uri_async(uri, this._iconBox._size, this._iconBox._size);
+        } else {
+            this._iconBox.child = new St.Icon({ icon_name: 'avatar-default',
+                                                icon_size: this._iconBox._size });
+        }
+
+        return this._iconBox;
+    },
+
+    createSecondaryIcon: function() {
+        let iconBox = new St.Bin();
+        iconBox.child = new St.Icon({ style_class: 'secondary-icon' });
+        let presenceType = this._contact.get_presence_type();
+
+        switch (presenceType) {
+            case Tp.ConnectionPresenceType.AVAILABLE:
+                iconBox.child.icon_name = 'user-available';
+                break;
+            case Tp.ConnectionPresenceType.BUSY:
+                iconBox.child.icon_name = 'user-busy';
+                break;
+            case Tp.ConnectionPresenceType.OFFLINE:
+                iconBox.child.icon_name = 'user-offline';
+                break;
+            case Tp.ConnectionPresenceType.HIDDEN:
+                iconBox.child.icon_name = 'user-invisible';
+                break;
+            case Tp.ConnectionPresenceType.AWAY:
+                iconBox.child.icon_name = 'user-away';
+                break;
+            case Tp.ConnectionPresenceType.EXTENDED_AWAY:
+                iconBox.child.icon_name = 'user-idle';
+                break;
+            default:
+                iconBox.child.icon_name = 'user-offline';
+       }
+       return iconBox;
+    },
+
+    _updateAvatarIcon: function() {
+        this.iconUpdated();
+        this._notification.update(this._notification.title, null, { customContent: true });
+    },
+
+    open: function(notification) {
+          if (this._client.is_handling_channel(this._channel)) {
+              // We are handling the channel, try to pass it to Empathy
+              this._client.delegate_channels_async([this._channel], global.get_current_time(), '', null);
+          }
+          else {
+              // We are not the handler, just ask to present the channel
+              let dbus = Tp.DBusDaemon.dup();
+              let cd = Tp.ChannelDispatcher.new(dbus);
+
+              cd.present_channel_async(this._channel, global.get_current_time(), null);
+          }
+    },
+
+    _getLogMessages: function() {
+        let logManager = Tpl.LogManager.dup_singleton();
+        let entity = Tpl.Entity.new_from_tp_contact(this._contact, Tpl.EntityType.CONTACT);
+
+        logManager.get_filtered_events_async(this._account, entity,
+                                             Tpl.EventTypeMask.TEXT, SCROLLBACK_HISTORY_LINES,
+                                             null, Lang.bind(this, this._displayPendingMessages));
+    },
+
+    _displayPendingMessages: function(logManager, result) {
+        let [success, events] = logManager.get_filtered_events_finish(result);
+
+        let logMessages = events.map(makeMessageFromTplEvent);
+
+        let pendingTpMessages = this._channel.get_pending_messages();
+        let pendingMessages = [];
+
+        for (let i = 0; i < pendingTpMessages.length; i++) {
+            let message = pendingTpMessages[i];
+
+            if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
+                continue;
+
+            pendingMessages.push(makeMessageFromTpMessage(message, NotificationDirection.RECEIVED));
+
+            this._pendingMessages.push(message);
+        }
+
+        this.countUpdated();
+
+        let showTimestamp = false;
+
+        for (let i = 0; i < logMessages.length; i++) {
+            let logMessage = logMessages[i];
+            let isPending = false;
+
+            // Skip any log messages that are also in pendingMessages
+            for (let j = 0; j < pendingMessages.length; j++) {
+                let pending = pendingMessages[j];
+                if (logMessage.timestamp == pending.timestamp && logMessage.text == pending.text) {
+                    isPending = true;
+                    break;
+                }
+            }
+
+            if (!isPending) {
+                showTimestamp = true;
+                this._notification.appendMessage(logMessage, true, ['chat-log-message']);
+            }
+        }
+
+        if (showTimestamp)
+            this._notification.appendTimestamp();
+
+        for (let i = 0; i < pendingMessages.length; i++)
+            this._notification.appendMessage(pendingMessages[i], true);
+
+        if (pendingMessages.length > 0)
+            this.notify();
+    },
+
+    _channelClosed: function() {
+        this._channel.disconnect(this._closedId);
+        this._channel.disconnect(this._receivedId);
+        this._channel.disconnect(this._pendingId);
+        this._channel.disconnect(this._sentId);
+
+        this._contact.disconnect(this._notifyAliasId);
+        this._contact.disconnect(this._notifyAvatarId);
+        this._contact.disconnect(this._presenceChangedId);
+
+        this.destroy();
+    },
+
+    /* All messages are new messages for Telepathy sources */
+    get count() {
+        return this._pendingMessages.length;
+    },
+
+    get unseenCount() {
+        return this.count;
+    },
+
+    get countVisible() {
+        return this.count > 0;
+    },
+
+    _messageReceived: function(channel, message) {
+        if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
+            return;
+
+        this._pendingMessages.push(message);
+        this.countUpdated();
+
+        message = makeMessageFromTpMessage(message, NotificationDirection.RECEIVED);
+        this._notification.appendMessage(message);
+
+        // Wait a bit before notifying for the received message, a handler
+        // could ack it in the meantime.
+        if (this._notifyTimeoutId != 0)
+            Mainloop.source_remove(this._notifyTimeoutId);
+        this._notifyTimeoutId = Mainloop.timeout_add(500,
+            Lang.bind(this, this._notifyTimeout));
+    },
+
+    _notifyTimeout: function() {
+        if (this._pendingMessages.length != 0)
+            this.notify();
+
+        this._notifyTimeoutId = 0;
+
+        return false;
+    },
+
+    // This is called for both messages we send from
+    // our client and other clients as well.
+    _messageSent: function(channel, message, flags, token) {
+        message = makeMessageFromTpMessage(message, NotificationDirection.SENT);
+        this._notification.appendMessage(message);
+    },
+
+    notify: function() {
+        this.parent(this._notification);
+    },
+
+    respond: function(text) {
+        let type;
+        if (text.slice(0, 4) == '/me ') {
+            type = Tp.ChannelTextMessageType.ACTION;
+            text = text.slice(4);
+        } else {
+            type = Tp.ChannelTextMessageType.NORMAL;
+        }
+
+        let msg = Tp.ClientMessage.new_text(type, text);
+        this._channel.send_message_async(msg, 0, Lang.bind(this, function (src, result) {
+            this._channel.send_message_finish(result); 
+        }));
+    },
+
+    setChatState: function(state) {
+        // We don't want to send COMPOSING every time a letter is typed into
+        // the entry. We send the state only when it changes. Telepathy/Empathy
+        // might change it behind our back if the user is using both
+        // gnome-shell's entry and the Empathy conversation window. We could
+        // keep track of it with the ChatStateChanged signal but it is good
+        // enough right now.
+        if (state != this._chatState) {
+          this._chatState = state;
+          this._channel.set_chat_state_async(state, null);
+        }
+    },
+
+    _presenceChanged: function (contact, presence, status, message) {
+        let msg, title;
+
+        title = GLib.markup_escape_text(this.title, -1);
+
+        this._notification.update(this._notification.title, null, { customContent: true, secondaryIcon: this.createSecondaryIcon() });
+
+        if (message)
+            msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
+    },
+
+    _pendingRemoved: function(channel, message) {
+        let idx = this._pendingMessages.indexOf(message);
+
+        if (idx >= 0) {
+            this._pendingMessages.splice(idx, 1);
+            this.countUpdated();
+        }
+        else
+            throw new Error('Message not in our pending list: ' + message);
+    },
+
+    _ackMessages: function() {
+        // Don't clear our messages here, tp-glib will send a
+        // 'pending-message-removed' for each one.
+        this._channel.ack_all_pending_messages_async(Lang.bind(this, function(src, result) {
+            this._channel.ack_all_pending_messages_finish(result);}));
+    }
+});
+
+const ChatNotification = new Lang.Class({
+    Name: 'ChatNotification',
+    Extends: MessageTray.Notification,
+
+    _init: function(source) {
+        this.parent(source, source.title, null, { customContent: true, secondaryIcon: source.createSecondaryIcon() });
+        this.setResident(true);
+
+        this._responseEntry = new St.Entry({ style_class: 'chat-response',
+                                             can_focus: true });
+        this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
+        this._responseEntry.clutter_text.connect('text-changed', Lang.bind(this, this._onEntryChanged));
+        this.setActionArea(this._responseEntry);
+
+        this._oldMaxScrollAdjustment = 0;
+        this._createScrollArea();
+        this._lastGroup = null;
+        this._lastGroupActor = null;
+
+        this._scrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
+            let currentValue = adjustment.value + adjustment.page_size;
+            if (currentValue == this._oldMaxScrollAdjustment)
+                this.scrollTo(St.Side.BOTTOM);
+            this._oldMaxScrollAdjustment = adjustment.upper;
+        }));
+
+        this._inputHistory = new History.HistoryManager({ entry: this._responseEntry.clutter_text });
+
+        this._history = [];
+        this._timestampTimeoutId = 0;
+        this._composingTimeoutId = 0;
+    },
+
+    /**
+     * appendMessage:
+     * @message: An object with the properties:
+     *   text: the body of the message,
+     *   messageType: a #Tp.ChannelTextMessageType,
+     *   sender: the name of the sender,
+     *   timestamp: the time the message was sent
+     *   direction: a #NotificationDirection
+     * 
+     * @noTimestamp: Whether to add a timestamp. If %true, no timestamp
+     *   will be added, regardless of the difference since the
+     *   last timestamp
+     */
+    appendMessage: function(message, noTimestamp) {
+        let messageBody = GLib.markup_escape_text(message.text, -1);
+        let styles = [message.direction];
+
+        if (message.messageType == Tp.ChannelTextMessageType.ACTION) {
+            let senderAlias = GLib.markup_escape_text(message.sender, -1);
+            messageBody = '<i>%s</i> %s'.format(senderAlias, messageBody);
+            styles.push('chat-action');
+        }
+
+        if (message.direction == NotificationDirection.RECEIVED) {
+            this.update(this.source.title, messageBody, { customContent: true,
+                                                          bannerMarkup: true });
+        }
+
+        let group = (message.direction == NotificationDirection.RECEIVED ?
+                     'received' : 'sent');
+
+        this._append({ body: messageBody,
+                       group: group,
+                       styles: styles,
+                       timestamp: message.timestamp,
+                       noTimestamp: noTimestamp });
+    },
+
+    _filterMessages: function() {
+        if (this._history.length < 1)
+            return;
+
+        let lastMessageTime = this._history[0].time;
+        let currentTime = (Date.now() / 1000);
+
+        // Keep the scrollback from growing too long. If the most
+        // recent message (before the one we just added) is within
+        // SCROLLBACK_RECENT_TIME, we will keep
+        // SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
+        // we'll keep SCROLLBACK_IDLE_LENGTH messages.
+
+        let maxLength = (lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME) ?
+            SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
+
+        let filteredHistory = this._history.filter(function(item) { return item.realMessage });
+        if (filteredHistory.length > maxLength) {
+            let lastMessageToKeep = filteredHistory[maxLength];
+            let expired = this._history.splice(this._history.indexOf(lastMessageToKeep));
+            for (let i = 0; i < expired.length; i++)
+                expired[i].actor.destroy();
+        }
+
+        let groups = this._contentArea.get_children();
+        for (let i = 0; i < groups.length; i++) {
+            let group = groups[i];
+            if (group.get_n_children() == 0)
+                group.destroy();
+        }
+    },
+
+    /**
+     * _append:
+     * @props: An object with the properties:
+     *  body: The text of the message.
+     *  group: The group of the message, one of:
+     *         'received', 'sent', 'meta'.
+     *  styles: Style class names for the message to have.
+     *  timestamp: The timestamp of the message.
+     *  noTimestamp: suppress timestamp signal?
+     *  childProps: props to add the actor with.
+     */
+    _append: function(props) {
+        let currentTime = (Date.now() / 1000);
+        props = Params.parse(props, { body: null,
+                                      group: null,
+                                      styles: [],
+                                      timestamp: currentTime,
+                                      noTimestamp: false,
+                                      childProps: null });
+
+        // Reset the old message timeout
+        if (this._timestampTimeoutId)
+            Mainloop.source_remove(this._timestampTimeoutId);
+
+        let highlighter = new MessageTray.URLHighlighter(props.body,
+                                                         true,  // line wrap?
+                                                         true); // allow markup?
+
+        let body = highlighter.actor;
+
+        let styles = props.styles;
+        for (let i = 0; i < styles.length; i++)
+            body.add_style_class_name(styles[i]);
+
+        let group = props.group;
+        if (group != this._lastGroup) {
+            let style = 'chat-group-' + group;
+            this._lastGroup = group;
+            this._lastGroupActor = new St.BoxLayout({ style_class: style,
+                                                      vertical: true });
+            this.addActor(this._lastGroupActor);
+        }
+
+        this._lastGroupActor.add(body, props.childProps);
+
+        this.updated();
+
+        let timestamp = props.timestamp;
+        this._history.unshift({ actor: body, time: timestamp,
+                                realMessage: group != 'meta' });
+
+        if (!props.noTimestamp) {
+            if (timestamp < currentTime - SCROLLBACK_IMMEDIATE_TIME)
+                this.appendTimestamp();
+            else
+                // Schedule a new timestamp in SCROLLBACK_IMMEDIATE_TIME
+                // from the timestamp of the message.
+                this._timestampTimeoutId = Mainloop.timeout_add_seconds(
+                    SCROLLBACK_IMMEDIATE_TIME - (currentTime - timestamp),
+                    Lang.bind(this, this.appendTimestamp));
+        }
+
+        this._filterMessages();
+    },
+
+    _formatTimestamp: function(date) {
+        let now = new Date();
+
+        var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
+
+        let format;
+
+        // Show a week day and time if date is in the last week
+        if (daysAgo < 1 || (daysAgo < 7 && now.getDay() != date.getDay())) {
+            /* Translators: this is a time format string followed by a date.
+             If applicable, replace %X with a strftime format valid for your
+             locale, without seconds. */
+            // xgettext:no-c-format
+            format = _("Sent at <b>%X</b> on <b>%A</b>");
+
+        } else if (date.getYear() == now.getYear()) {
+            /* Translators: this is a time format in the style of "Wednesday, May 25",
+             shown when you get a chat message in the same year. */
+            // xgettext:no-c-format
+            format = _("Sent on <b>%A</b>, <b>%B %d</b>");
+        } else {
+            /* Translators: this is a time format in the style of "Wednesday, May 25, 2012",
+             shown when you get a chat message in a different year. */
+            // xgettext:no-c-format
+            format = _("Sent on <b>%A</b>, <b>%B %d</b>, %Y");
+        }
+
+        return date.toLocaleFormat(format);
+    },
+
+    appendTimestamp: function() {
+        let lastMessageTime = this._history[0].time;
+        let lastMessageDate = new Date(lastMessageTime * 1000);
+
+        let timeLabel = this._append({ body: this._formatTimestamp(lastMessageDate),
+                                       group: 'meta',
+                                       styles: ['chat-meta-message'],
+                                       childProps: { expand: true, x_fill: false,
+                                                     x_align: St.Align.END },
+                                       noTimestamp: true,
+                                       timestamp: lastMessageTime });
+
+        this._filterMessages();
+
+        return false;
+    },
+
+    appendAliasChange: function(oldAlias, newAlias) {
+        oldAlias = GLib.markup_escape_text(oldAlias, -1);
+        newAlias = GLib.markup_escape_text(newAlias, -1);
+
+        /* Translators: this is the other person changing their old IM name to their new
+           IM name. */
+        let message = '<i>' + _("%s is now known as %s").format(oldAlias, newAlias) + '</i>';
+
+        let label = this._append({ body: message,
+                                   group: 'meta',
+                                   styles: ['chat-meta-message'] });
+
+        this.update(newAlias, null, { customContent: true });
+
+        this._filterMessages();
+    },
+
+    _onEntryActivated: function() {
+        let text = this._responseEntry.get_text();
+        if (text == '')
+            return;
+
+        this._inputHistory.addItem(text);
+
+        // Telepathy sends out the Sent signal for us.
+        // see Source._messageSent
+        this._responseEntry.set_text('');
+        this.source.respond(text);
+    },
+
+    _composingStopTimeout: function() {
+        this._composingTimeoutId = 0;
+
+        this.source.setChatState(Tp.ChannelChatState.PAUSED);
+
+        return false;
+    },
+
+    _onEntryChanged: function() {
+        let text = this._responseEntry.get_text();
+
+        // If we're typing, we want to send COMPOSING.
+        // If we empty the entry, we want to send ACTIVE.
+        // If we've stopped typing for COMPOSING_STOP_TIMEOUT
+        //    seconds, we want to send PAUSED.
+
+        // Remove composing timeout.
+        if (this._composingTimeoutId > 0) {
+            Mainloop.source_remove(this._composingTimeoutId);
+            this._composingTimeoutId = 0;
+        }
+
+        if (text != '') {
+            this.source.setChatState(Tp.ChannelChatState.COMPOSING);
+
+            this._composingTimeoutId = Mainloop.timeout_add_seconds(
+                COMPOSING_STOP_TIMEOUT,
+                Lang.bind(this, this._composingStopTimeout));
+        } else {
+            this.source.setChatState(Tp.ChannelChatState.ACTIVE);
+        }
+    }
+});
+
+const ApproverSource = new Lang.Class({
+    Name: 'ApproverSource',
+    Extends: MessageTray.Source,
+
+    _init: function(dispatchOp, text, gicon) {
+        this._gicon = gicon;
+
+        this.parent(text);
+
+        this._dispatchOp = dispatchOp;
+
+        // Destroy the source if the channel dispatch operation is invalidated
+        // as we can't approve any more.
+        this._invalidId = dispatchOp.connect('invalidated',
+                                             Lang.bind(this, function(domain, code, msg) {
+            this.destroy();
+        }));
+    },
+
+    destroy: function() {
+        if (this._invalidId != 0) {
+            this._dispatchOp.disconnect(this._invalidId);
+            this._invalidId = 0;
+        }
+
+        this.parent();
+    },
+
+    createIcon: function(size) {
+        return new St.Icon({ gicon: this._gicon,
+                             icon_size: size });
+    }
+});
+
+const RoomInviteNotification = new Lang.Class({
+    Name: 'RoomInviteNotification',
+    Extends: MessageTray.Notification,
+
+    _init: function(source, dispatchOp, channel, inviter) {
+        this.parent(source,
+                    /* translators: argument is a room name like
+                     * room@jabber.org for example. */
+                    _("Invitation to %s").format(channel.get_identifier()),
+                    null,
+                    { customContent: true });
+        this.setResident(true);
+
+        /* translators: first argument is the name of a contact and the second
+         * one the name of a room. "Alice is inviting you to join room@jabber.org
+         * for example. */
+        this.addBody(_("%s is inviting you to join %s").format(inviter.get_alias(), channel.get_identifier()));
+
+        this.addButton('decline', _("Decline"));
+        this.addButton('accept', _("Accept"));
+
+        this.connect('action-invoked', Lang.bind(this, function(self, action) {
+            switch (action) {
+            case 'decline':
+                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
+                                                '', function(src, result) {
+                    src.leave_channels_finish(result)});
+                break;
+            case 'accept':
+                dispatchOp.handle_with_time_async('', global.get_current_time(),
+                                                  function(src, result) {
+                    src.handle_with_time_finish(result)});
+                break;
+            }
+            this.destroy();
+        }));
+    }
+});
+
+// Audio Video
+const AudioVideoNotification = new Lang.Class({
+    Name: 'AudioVideoNotification',
+    Extends: MessageTray.Notification,
+
+    _init: function(source, dispatchOp, channel, contact, isVideo) {
+        let title = '';
+
+        if (isVideo)
+             /* translators: argument is a contact name like Alice for example. */
+            title = _("Video call from %s").format(contact.get_alias());
+        else
+             /* translators: argument is a contact name like Alice for example. */
+            title = _("Call from %s").format(contact.get_alias());
+
+        this.parent(source, title, null, { customContent: true });
+        this.setResident(true);
+
+        this.addButton('reject', _("Reject"));
+        /* translators: this is a button label (verb), not a noun */
+        this.addButton('answer', _("Answer"));
+
+        this.connect('action-invoked', Lang.bind(this, function(self, action) {
+            switch (action) {
+            case 'reject':
+                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
+                                                '', function(src, result) {
+                    src.leave_channels_finish(result)});
+                break;
+            case 'answer':
+                dispatchOp.handle_with_time_async('', global.get_current_time(),
+                                                  function(src, result) {
+                    src.handle_with_time_finish(result)});
+                break;
+            }
+            this.destroy();
+        }));
+    }
+});
+
+// File Transfer
+const FileTransferNotification = new Lang.Class({
+    Name: 'FileTransferNotification',
+    Extends: MessageTray.Notification,
+
+    _init: function(source, dispatchOp, channel, contact) {
+        this.parent(source,
+                    /* To translators: The first parameter is
+                     * the contact's alias and the second one is the
+                     * file name. The string will be something
+                     * like: "Alice is sending you test.ogg"
+                     */
+                    _("%s is sending you %s").format(contact.get_alias(),
+                                                     channel.get_filename()),
+                    null,
+                    { customContent: true });
+        this.setResident(true);
+
+        this.addButton('decline', _("Decline"));
+        this.addButton('accept', _("Accept"));
+
+        this.connect('action-invoked', Lang.bind(this, function(self, action) {
+            switch (action) {
+            case 'decline':
+                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
+                                                '', function(src, result) {
+                    src.leave_channels_finish(result)});
+                break;
+            case 'accept':
+                dispatchOp.handle_with_time_async('', global.get_current_time(),
+                                                  function(src, result) {
+                    src.handle_with_time_finish(result)});
+                break;
+            }
+            this.destroy();
+        }));
+    }
+});
+
+// Subscription request
+const SubscriptionRequestNotification = new Lang.Class({
+    Name: 'SubscriptionRequestNotification',
+    Extends: MessageTray.Notification,
+
+    _init: function(source, contact) {
+        this.parent(source,
+                    /* To translators: The parameter is the contact's alias */
+                    _("%s would like permission to see when you are online").format(contact.get_alias()),
+                    null, { customContent: true });
+
+        this._contact = contact;
+        this._connection = contact.get_connection();
+
+        let layout = new St.BoxLayout({ vertical: false });
+
+        // Display avatar
+        let iconBox = new St.Bin({ style_class: 'avatar-box' });
+        iconBox._size = 48;
+
+        let textureCache = St.TextureCache.get_default();
+        let file = contact.get_avatar_file();
+
+        if (file) {
+            let uri = file.get_uri();
+            iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size);
+        }
+        else {
+            iconBox.child = new St.Icon({ icon_name: 'avatar-default',
+                                          icon_size: iconBox._size });
+        }
+
+        layout.add(iconBox);
+
+        // subscription request message
+        let label = new St.Label({ style_class: 'subscription-message',
+                                   text: contact.get_publish_request() });
+
+        layout.add(label);
+
+        this.addActor(layout);
+
+        this.addButton('decline', _("Decline"));
+        this.addButton('accept', _("Accept"));
+
+        this.connect('action-invoked', Lang.bind(this, function(self, action) {
+            switch (action) {
+            case 'decline':
+                contact.remove_async(function(src, result) {
+                    src.remove_finish(result)});
+                break;
+            case 'accept':
+                // Authorize the contact and request to see his status as well
+                contact.authorize_publication_async(function(src, result) {
+                    src.authorize_publication_finish(result)});
+
+                contact.request_subscription_async('', function(src, result) {
+                    src.request_subscription_finish(result)});
+                break;
+            }
+
+            // rely on _subscriptionStatesChangedCb to destroy the
+            // notification
+        }));
+
+        this._changedId = contact.connect('subscription-states-changed',
+            Lang.bind(this, this._subscriptionStatesChangedCb));
+        this._invalidatedId = this._connection.connect('invalidated',
+            Lang.bind(this, this.destroy));
+    },
+
+    destroy: function() {
+        if (this._changedId != 0) {
+            this._contact.disconnect(this._changedId);
+            this._changedId = 0;
+        }
+
+        if (this._invalidatedId != 0) {
+            this._connection.disconnect(this._invalidatedId);
+            this._invalidatedId = 0;
+        }
+
+        this.parent();
+    },
+
+    _subscriptionStatesChangedCb: function(contact, subscribe, publish, msg) {
+        // Destroy the notification if the subscription request has been
+        // answered
+        if (publish != Tp.SubscriptionState.ASK)
+            this.destroy();
+    }
+});
+
+// Messages from empathy/libempathy/empathy-utils.c
+// create_errors_to_message_hash()
+
+/* Translator note: these should be the same messages that are
+ * used in Empathy, so just copy and paste from there. */
+let _connectionErrorMessages = {};
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.NETWORK_ERROR)]
+  = _("Network error");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.AUTHENTICATION_FAILED)]
+  = _("Authentication failed");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_ERROR)]
+  = _("Encryption error");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_PROVIDED)]
+  = _("Certificate not provided");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_UNTRUSTED)]
+  = _("Certificate untrusted");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_EXPIRED)]
+  = _("Certificate expired");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED)]
+  = _("Certificate not activated");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_HOSTNAME_MISMATCH)]
+  = _("Certificate hostname mismatch");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_FINGERPRINT_MISMATCH)]
+  = _("Certificate fingerprint mismatch");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_SELF_SIGNED)]
+  = _("Certificate self-signed");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CANCELLED)]
+  = _("Status is set to offline");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_NOT_AVAILABLE)]
+  = _("Encryption is not available");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INVALID)]
+  = _("Certificate is invalid");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REFUSED)]
+  = _("Connection has been refused");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_FAILED)]
+  = _("Connection can't be established");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_LOST)]
+  = _("Connection has been lost");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ALREADY_CONNECTED)]
+  = _("This account is already connected to the server");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REPLACED)]
+  = _("Connection has been replaced by a new connection using the same resource");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.REGISTRATION_EXISTS)]
+  = _("The account already exists on the server");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.SERVICE_BUSY)]
+  = _("Server is currently too busy to handle the connection");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_REVOKED)]
+  = _("Certificate has been revoked");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INSECURE)]
+  = _("Certificate uses an insecure cipher algorithm or is cryptographically weak");
+_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)]
+  = _("The length of the server certificate, or the depth of the server certificate chain, exceed the limits imposed by the cryptography library");
+_connectionErrorMessages['org.freedesktop.DBus.Error.NoReply']
+  = _("Internal error");
+
+const AccountNotification = new Lang.Class({
+    Name: 'AccountNotification',
+    Extends: MessageTray.Notification,
+
+    _init: function(source, account, connectionError) {
+        this.parent(source,
+                    /* translators: argument is the account name, like
+                     * name@jabber.org for example. */
+                    _("Connection to %s failed").format(account.get_display_name()),
+                    null, { customContent: true });
+
+        this._label = new St.Label();
+        this.addActor(this._label);
+        this._updateMessage(connectionError);
+
+        this._account = account;
+
+        this.addButton('reconnect', _("Reconnect"));
+        this.addButton('edit', _("Edit account"));
+
+        this.connect('action-invoked', Lang.bind(this, function(self, action) {
+            switch (action) {
+            case 'reconnect':
+                // If it fails again, a new notification should pop up with the
+                // new error.
+                account.reconnect_async(null);
+                break;
+            case 'edit':
+                let cmd = '/usr/bin/empathy-accounts'
+                        + ' --select-account=%s'
+                        .format(account.get_path_suffix());
+                let app_info = Gio.app_info_create_from_commandline(cmd, null, 0);
+                app_info.launch([], global.create_app_launch_context());
+                break;
+            }
+            this.destroy();
+        }));
+
+        this._enabledId = account.connect('notify::enabled',
+                                          Lang.bind(this, function() {
+                                              if (!account.is_enabled())
+                                                  this.destroy();
+                                          }));
+
+        this._invalidatedId = account.connect('invalidated',
+                                              Lang.bind(this, this.destroy));
+
+        this._connectionStatusId = account.connect('notify::connection-status',
+            Lang.bind(this, function() {
+                let status = account.connection_status;
+                if (status == Tp.ConnectionStatus.CONNECTED) {
+                    this.destroy();
+                } else if (status == Tp.ConnectionStatus.DISCONNECTED) {
+                    this._updateMessage(account.connection_error);
+                }
+            }));
+    },
+
+    _updateMessage: function(connectionError) {
+        let message;
+        if (connectionError in _connectionErrorMessages) {
+            message = _connectionErrorMessages[connectionError];
+        } else {
+            message = _("Unknown reason");
+        }
+        this._label.set_text(message);
+    },
+
+    destroy: function() {
+        if (this._enabledId != 0) {
+            this._account.disconnect(this._enabledId);
+            this._enabledId = 0;
+        }
+
+        if (this._invalidatedId != 0) {
+            this._account.disconnect(this._invalidatedId);
+            this._invalidatedId = 0;
+        }
+
+        if (this._connectionStatusId != 0) {
+            this._account.disconnect(this._connectionStatusId);
+            this._connectionStatusId = 0;
+        }
+
+        this.parent();
+    }
+});
+const Component = TelepathyClient;
diff --git a/js/ui/keyringPrompt.js b/js/ui/keyringPrompt.js
deleted file mode 100644 (file)
index 814c946..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const Lang = imports.lang;
-const Shell = imports.gi.Shell;
-const Clutter = imports.gi.Clutter;
-const St = imports.gi.St;
-const Pango = imports.gi.Pango;
-const Gio = imports.gi.Gio;
-const GObject = imports.gi.GObject;
-const Gcr = imports.gi.Gcr;
-
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
-const CheckBox = imports.ui.checkBox;
-
-let prompter = null;
-
-const KeyringDialog = new Lang.Class({
-    Name: 'KeyringDialog',
-    Extends: ModalDialog.ModalDialog,
-
-    _init: function() {
-        this.parent({ styleClass: 'prompt-dialog' });
-
-        this.prompt = new Shell.KeyringPrompt();
-        this.prompt.connect('show-password', Lang.bind(this, this._onShowPassword));
-        this.prompt.connect('show-confirm', Lang.bind(this, this._onShowConfirm));
-        this.prompt.connect('hide-prompt', Lang.bind(this, this._onHidePrompt));
-
-        let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
-                                                vertical: false });
-        this.contentLayout.add(mainContentBox);
-
-        let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
-        mainContentBox.add(icon,
-                           { x_fill:  true,
-                             y_fill:  false,
-                             x_align: St.Align.END,
-                             y_align: St.Align.START });
-
-        this._messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
-                                              vertical: true });
-        mainContentBox.add(this._messageBox,
-                           { y_align: St.Align.START, expand: true, x_fill: true, y_fill: true });
-
-        let subject = new St.Label({ style_class: 'prompt-dialog-headline' });
-        this.prompt.bind_property('message', subject, 'text', GObject.BindingFlags.SYNC_CREATE);
-
-        this._messageBox.add(subject,
-                             { y_fill:  false,
-                               y_align: St.Align.START });
-
-        let description = new St.Label({ style_class: 'prompt-dialog-description' });
-        description.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
-        description.clutter_text.line_wrap = true;
-        this.prompt.bind_property('description', description, 'text', GObject.BindingFlags.SYNC_CREATE);
-        this._messageBox.add(description,
-                            { y_fill:  true,
-                              y_align: St.Align.START });
-
-        this._controlTable = null;
-
-        let buttons = [{ label: '',
-                         action: Lang.bind(this, this._onCancelButton),
-                         key:    Clutter.Escape
-                       },
-                       { label: '',
-                         action: Lang.bind(this, this._onContinueButton),
-                         default: true
-                       }]
-
-        this.setButtons(buttons);
-        this._cancelButton = buttons[0].button;
-        this._continueButton = buttons[1].button;
-
-        this.prompt.bind_property('cancel-label', this._cancelButton, 'label', GObject.BindingFlags.SYNC_CREATE);
-        this.prompt.bind_property('continue-label', this._continueButton, 'label', GObject.BindingFlags.SYNC_CREATE);
-    },
-
-    _buildControlTable: function() {
-        let table = new St.Table({ style_class: 'keyring-dialog-control-table' });
-        let row = 0;
-
-        if (this.prompt.password_visible) {
-            let label = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
-            label.set_text(_("Password:"));
-            table.add(label, { row: row, col: 0,
-                               x_expand: false, x_fill: true,
-                               x_align: St.Align.START,
-                               y_fill: false, y_align: St.Align.MIDDLE });
-            this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
-                                                 text: '',
-                                                 can_focus: true});
-            this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
-            ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
-            this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onPasswordActivate));
-            table.add(this._passwordEntry, { row: row, col: 1, x_expand: true, x_fill: true, x_align: St.Align.START });
-            row++;
-        } else {
-            this._passwordEntry = null;
-        }
-
-        if (this.prompt.confirm_visible) {
-            var label = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
-            label.set_text(_("Type again:"));
-            table.add(label, { row: row, col: 0,
-                               x_expand: false, x_fill: true,
-                               x_align: St.Align.START,
-                               y_fill: false, y_align: St.Align.MIDDLE });
-            this._confirmEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
-                                                text: '',
-                                                can_focus: true});
-            this._confirmEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
-            ShellEntry.addContextMenu(this._confirmEntry, { isPassword: true });
-            this._confirmEntry.clutter_text.connect('activate', Lang.bind(this, this._onConfirmActivate));
-            table.add(this._confirmEntry, { row: row, col: 1, x_expand: true, x_fill: true, x_align: St.Align.START });
-            row++;
-        } else {
-            this._confirmEntry = null;
-        }
-
-        this.prompt.set_password_actor(this._passwordEntry ? this._passwordEntry.clutter_text : null);
-        this.prompt.set_confirm_actor(this._confirmEntry ? this._confirmEntry.clutter_text : null);
-
-        if (this.prompt.choice_visible) {
-            let choice = new CheckBox.CheckBox();
-            this.prompt.bind_property('choice-label', choice.getLabelActor(), 'text', GObject.BindingFlags.SYNC_CREATE);
-            this.prompt.bind_property('choice-chosen', choice.actor, 'checked', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);
-            table.add(choice.actor, { row: row, col: 1, x_expand: false, x_fill: true, x_align: St.Align.START });
-            row++;
-        }
-
-        let warning = new St.Label({ style_class: 'prompt-dialog-error-label' });
-        warning.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
-        warning.clutter_text.line_wrap = true;
-        table.add(warning, { row: row, col: 1, x_expand: false, x_fill: false, x_align: St.Align.START });
-        this.prompt.bind_property('warning-visible', warning, 'visible', GObject.BindingFlags.SYNC_CREATE);
-        this.prompt.bind_property('warning', warning, 'text', GObject.BindingFlags.SYNC_CREATE);
-
-        if (this._controlTable) {
-            this._controlTable.destroy_all_children();
-            this._controlTable.destroy();
-        }
-
-        this._controlTable = table;
-        this._messageBox.add(table, { x_fill: true, y_fill: true });
-    },
-
-    _ensureOpen: function() {
-        // NOTE: ModalDialog.open() is safe to call if the dialog is
-        // already open - it just returns true without side-effects
-        if (this.open())
-          return true;
-
-        // The above fail if e.g. unable to get input grab
-        //
-        // In an ideal world this wouldn't happen (because the
-        // Shell is in complete control of the session) but that's
-        // just not how things work right now.
-
-        log('keyringPrompt: Failed to show modal dialog.' +
-            ' Dismissing prompt request');
-        this.prompt.cancel()
-        return false;
-    },
-
-    _onShowPassword: function(prompt) {
-        this._buildControlTable();
-        this._ensureOpen();
-        this._passwordEntry.grab_key_focus();
-    },
-
-    _onShowConfirm: function(prompt) {
-        this._buildControlTable();
-        this._ensureOpen();
-        this._continueButton.grab_key_focus();
-    },
-
-    _onHidePrompt: function(prompt) {
-        this.close();
-    },
-
-    _onPasswordActivate: function() {
-        if (this.prompt.confirm_visible)
-            this._confirmEntry.grab_key_focus();
-        else
-            this._onContinueButton();
-    },
-
-    _onConfirmActivate: function() {
-        this._onContinueButton();
-    },
-
-    _onContinueButton: function() {
-        this.prompt.complete()
-    },
-
-    _onCancelButton: function() {
-        this.prompt.cancel()
-    },
-});
-
-function init() {
-    prompter = new Gcr.SystemPrompter();
-    prompter.connect('new-prompt', function(prompter) {
-        let dialog = new KeyringDialog();
-        return dialog.prompt;
-    });
-
-    let connection = Gio.DBus.session;
-    prompter.register(connection);
-    Gio.bus_own_name_on_connection (connection, 'org.gnome.keyring.SystemPrompter',
-                                    Gio.BusNameOwnerFlags.REPLACE, null, null);
-}
index 1d8ad4c95085c3cfdfc48164dbf3bd2af55884f5..9410c2352dd4c2d9d561625a402438942e579101 100644 (file)
@@ -10,12 +10,9 @@ const Meta = imports.gi.Meta;
 const Shell = imports.gi.Shell;
 const St = imports.gi.St;
 
-const AutomountManager = imports.ui.automountManager;
-const AutorunManager = imports.ui.autorunManager;
+const Components = imports.ui.components;
 const CtrlAltTab = imports.ui.ctrlAltTab;
 const EndSessionDialog = imports.ui.endSessionDialog;
-const PolkitAuthenticationAgent = imports.ui.polkitAuthenticationAgent;
-const KeyringPrompt = imports.ui.keyringPrompt;
 const Environment = imports.ui.environment;
 const ExtensionSystem = imports.ui.extensionSystem;
 const ExtensionDownloader = imports.ui.extensionDownloader;
@@ -27,7 +24,6 @@ const PlaceDisplay = imports.ui.placeDisplay;
 const RunDialog = imports.ui.runDialog;
 const Layout = imports.ui.layout;
 const LookingGlass = imports.ui.lookingGlass;
-const NetworkAgent = imports.ui.networkAgent;
 const NotificationDaemon = imports.ui.notificationDaemon;
 const WindowAttentionHandler = imports.ui.windowAttentionHandler;
 const ScreenShield = imports.ui.screenShield;
@@ -35,7 +31,6 @@ const Scripting = imports.ui.scripting;
 const SessionMode = imports.ui.sessionMode;
 const ShellDBus = imports.ui.shellDBus;
 const ShellMountOperation = imports.ui.shellMountOperation;
-const TelepathyClient = imports.ui.telepathyClient;
 const UnlockDialog = imports.ui.unlockDialog;
 const WindowManager = imports.ui.windowManager;
 const Magnifier = imports.ui.magnifier;
@@ -45,8 +40,7 @@ const Util = imports.misc.util;
 const OVERRIDES_SCHEMA = 'org.gnome.shell.overrides';
 const DEFAULT_BACKGROUND_COLOR = Clutter.Color.from_pixel(0x2266bbff);
 
-let automountManager = null;
-let autorunManager = null;
+let componentManager = null;
 let panel = null;
 let overview = null;
 let runDialog = null;
@@ -56,9 +50,7 @@ let messageTray = null;
 let screenShield = null;
 let notificationDaemon = null;
 let windowAttentionHandler = null;
-let telepathyClient = null;
 let ctrlAltTabManager = null;
-let recorder = null;
 let sessionMode = null;
 let shellDBusService = null;
 let shellMountOpDBusService = null;
@@ -70,7 +62,6 @@ let magnifier = null;
 let xdndHandler = null;
 let keyboard = null;
 let layoutManager = null;
-let networkAgent = null;
 let _startDate;
 let _defaultCssStylesheet = null;
 let _cssStylesheet = null;
@@ -78,19 +69,6 @@ let _overridesSettings = null;
 
 let background = null;
 
-function createUserSession() {
-    telepathyClient = new TelepathyClient.Client();
-    automountManager = new AutomountManager.AutomountManager();
-    autorunManager = new AutorunManager.AutorunManager();
-    networkAgent = new NetworkAgent.NetworkAgent();
-
-    _initRecorder();
-}
-
-function createGDMSession() {
-    screenShield.showDialog();
-}
-
 function createGDMLoginDialog(parentActor) {
     // We do this this here instead of at the top to prevent GDM
     // related code from getting loaded in normal user sessions
@@ -105,47 +83,8 @@ function createSessionUnlockDialog(parentActor) {
     return [dialog, false];
 }
 
-function createInitialSetupSession() {
-    networkAgent = new NetworkAgent.NetworkAgent();
-}
-
-function _initRecorder() {
-    let recorderSettings = new Gio.Settings({ schema: 'org.gnome.shell.recorder' });
-    let desktopLockdownSettings = new Gio.Settings({ schema: 'org.gnome.desktop.lockdown' });
-    let bindingSettings = new Gio.Settings({ schema: 'org.gnome.shell.keybindings' });
-
-    global.display.add_keybinding('toggle-recording',
-                                  bindingSettings,
-                                  Meta.KeyBindingFlags.NONE, function() {
-        if (recorder == null) {
-            recorder = new Shell.Recorder({ stage: global.stage });
-        }
-
-        if (recorder.is_recording()) {
-            recorder.close();
-            Meta.enable_unredirect_for_screen(global.screen);
-        } else if (!desktopLockdownSettings.get_boolean('disable-save-to-disk')) {
-            // read the parameters from GSettings always in case they have changed
-            recorder.set_framerate(recorderSettings.get_int('framerate'));
-            /* Translators: this is a filename used for screencast recording */
-            // xgettext:no-c-format
-            recorder.set_filename(_("Screencast from %d %t") + '.' + recorderSettings.get_string('file-extension'));
-            let pipeline = recorderSettings.get_string('pipeline');
-
-            if (!pipeline.match(/^\s*$/))
-                recorder.set_pipeline(pipeline);
-            else
-                recorder.set_pipeline(null);
-
-            Meta.disable_unredirect_for_screen(global.screen);
-            recorder.record();
-        }
-    });
-}
-
 function _sessionUpdated() {
     Meta.keybindings_set_custom_handler('panel-run-dialog', sessionMode.hasRunDialog ? openRunDialog : null);
-    loadTheme();
 }
 
 function start() {
@@ -219,8 +158,7 @@ function start() {
     keyboard = new Keyboard.Keyboard();
     notificationDaemon = new NotificationDaemon.NotificationDaemon();
     windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
-
-    sessionMode.createSession();
+    componentManager = new Components.ComponentManager();
 
     layoutManager.init();
     keyboard.init();
@@ -238,12 +176,6 @@ function start() {
     // initiate logouts.
     EndSessionDialog.init();
 
-    // Attempt to become a PolicyKit authentication agent
-    PolkitAuthenticationAgent.init()
-
-    // Become a prompter for gnome keyring
-    KeyringPrompt.init();
-
     _startDate = new Date();
 
     global.stage.connect('captured-event', _globalKeyPressHandler);
diff --git a/js/ui/networkAgent.js b/js/ui/networkAgent.js
deleted file mode 100644 (file)
index dbfb758..0000000
+++ /dev/null
@@ -1,704 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/*
- * Copyright 2011 Giovanni Campagna <scampa.giovanni@gmail.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2, or (at your option)
- * any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
- * 02111-1307, USA.
- *
- */
-
-const Clutter = imports.gi.Clutter;
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const Lang = imports.lang;
-const NetworkManager = imports.gi.NetworkManager;
-const NMClient = imports.gi.NMClient;
-const Pango = imports.gi.Pango;
-const Shell = imports.gi.Shell;
-const St = imports.gi.St;
-
-const Config = imports.misc.config;
-const ModalDialog = imports.ui.modalDialog;
-const PopupMenu = imports.ui.popupMenu;
-const ShellEntry = imports.ui.shellEntry;
-
-const VPN_UI_GROUP = 'VPN Plugin UI';
-
-const NetworkSecretDialog = new Lang.Class({
-    Name: 'NetworkSecretDialog',
-    Extends: ModalDialog.ModalDialog,
-
-    _init: function(agent, requestId, connection, settingName, hints, contentOverride) {
-        this.parent({ styleClass: 'prompt-dialog' });
-
-        this._agent = agent;
-        this._requestId = requestId;
-        this._connection = connection;
-        this._settingName = settingName;
-        this._hints = hints;
-
-        if (contentOverride)
-            this._content = contentOverride;
-        else
-            this._content = this._getContent();
-
-        let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
-                                                vertical: false });
-        this.contentLayout.add(mainContentBox,
-                               { x_fill: true,
-                                 y_fill: true });
-
-        let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
-        mainContentBox.add(icon,
-                           { x_fill:  true,
-                             y_fill:  false,
-                             x_align: St.Align.END,
-                             y_align: St.Align.START });
-
-        let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
-                                            vertical: true });
-        mainContentBox.add(messageBox,
-                           { y_align: St.Align.START });
-
-        let subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
-                                            text: this._content.title });
-        messageBox.add(subjectLabel,
-                       { y_fill:  false,
-                         y_align: St.Align.START });
-
-        if (this._content.message != null) {
-            let descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
-                                                  text: this._content.message,
-                                                  // HACK: for reasons unknown to me, the label
-                                                  // is not asked the correct height for width,
-                                                  // and thus is underallocated
-                                                  // place a fixed height to avoid overflowing
-                                                  style: 'height: 3em'
-                                                });
-            descriptionLabel.clutter_text.line_wrap = true;
-
-            messageBox.add(descriptionLabel,
-                           { y_fill:  true,
-                             y_align: St.Align.START,
-                             expand: true });
-        }
-
-        let secretTable = new St.Table({ style_class: 'network-dialog-secret-table' });
-        let initialFocusSet = false;
-        let pos = 0;
-        for (let i = 0; i < this._content.secrets.length; i++) {
-            let secret = this._content.secrets[i];
-            let label = new St.Label({ style_class: 'prompt-dialog-password-label',
-                                       text: secret.label });
-
-            let reactive = secret.key != null;
-
-            secret.entry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
-                                          text: secret.value, can_focus: reactive,
-                                          reactive: reactive });
-            ShellEntry.addContextMenu(secret.entry,
-                                      { isPassword: secret.password });
-
-            if (secret.validate)
-                secret.valid = secret.validate(secret);
-            else // no special validation, just ensure it's not empty
-                secret.valid = secret.value.length > 0;
-
-            if (reactive) {
-                if (!initialFocusSet) {
-                    this.setInitialKeyFocus(secret.entry);
-                    initialFocusSet = true;
-                }
-
-                secret.entry.clutter_text.connect('activate', Lang.bind(this, this._onOk));
-                secret.entry.clutter_text.connect('text-changed', Lang.bind(this, function() {
-                    secret.value = secret.entry.get_text();
-                    if (secret.validate)
-                        secret.valid = secret.validate(secret);
-                    else
-                        secret.valid = secret.value.length > 0;
-                    this._updateOkButton();
-                }));
-            } else
-                secret.valid = true;
-
-            secretTable.add(label, { row: pos, col: 0,
-                                     x_expand: false, x_fill: true,
-                                     x_align: St.Align.START,
-                                     y_fill: false, y_align: St.Align.MIDDLE });
-            secretTable.add(secret.entry, { row: pos, col: 1, x_expand: true, x_fill: true, y_align: St.Align.END });
-            pos++;
-
-            if (secret.password)
-                secret.entry.clutter_text.set_password_char('\u25cf');
-        }
-
-        messageBox.add(secretTable);
-
-        this._okButton = { label:  _("Connect"),
-                           action: Lang.bind(this, this._onOk),
-                           default: true
-                         };
-
-        this.setButtons([{ label: _("Cancel"),
-                           action: Lang.bind(this, this.cancel),
-                           key:    Clutter.KEY_Escape,
-                         },
-                         this._okButton]);
-    },
-
-    _updateOkButton: function() {
-        let valid = true;
-        for (let i = 0; i < this._content.secrets.length; i++) {
-            let secret = this._content.secrets[i];
-            valid = valid && secret.valid;
-        }
-
-        this._okButton.button.reactive = valid;
-        this._okButton.button.can_focus = valid;
-    },
-
-    _onOk: function() {
-        let valid = true;
-        for (let i = 0; i < this._content.secrets.length; i++) {
-            let secret = this._content.secrets[i];
-            valid = valid && secret.valid;
-            if (secret.key != null)
-                this._agent.set_password(this._requestId, secret.key, secret.value);
-        }
-
-        if (valid) {
-            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
-            this.close(global.get_current_time());
-        }
-        // do nothing if not valid
-    },
-
-    cancel: function() {
-        this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
-        this.close(global.get_current_time());
-    },
-
-    _validateWpaPsk: function(secret) {
-        let value = secret.value;
-        if (value.length == 64) {
-            // must be composed of hexadecimal digits only
-            for (let i = 0; i < 64; i++) {
-                if (!((value[i] >= 'a' && value[i] <= 'f')
-                      || (value[i] >= 'A' && value[i] <= 'F')
-                      || (value[i] >= '0' && value[i] <= '9')))
-                    return false;
-            }
-            return true;
-        }
-
-        return (value.length >= 8 && value.length <= 63);
-    },
-
-    _validateStaticWep: function(secret) {
-        let value = secret.value;
-        if (secret.wep_key_type == NetworkManager.WepKeyType.KEY) {
-            if (value.length == 10 || value.length == 26) {
-               for (let i = 0; i < value.length; i++) {
-                    if (!((value[i] >= 'a' && value[i] <= 'f')
-                          || (value[i] >= 'A' && value[i] <= 'F')
-                          || (value[i] >= '0' && value[i] <= '9')))
-                        return false;
-               }
-           } else if (value.length == 5 || value.length == 13) {
-               for (let i = 0; i < value.length; i++) {
-                    if (!((value[i] >= 'a' && value[i] <= 'z')
-                          || (value[i] >= 'A' && value[i] <= 'Z')))
-                        return false;
-                }
-            } else
-                return false;
-       } else if (secret.wep_key_type == NetworkManager.WepKeyType.PASSPHRASE) {
-           if (value.length < 0 || value.length > 64)
-               return false;
-       }
-        return true;
-    },
-
-    _getWirelessSecrets: function(secrets, wirelessSetting) {
-        let wirelessSecuritySetting = this._connection.get_setting_wireless_security();
-        switch (wirelessSecuritySetting.key_mgmt) {
-        // First the easy ones
-        case 'wpa-none':
-        case 'wpa-psk':
-            secrets.push({ label: _("Password: "), key: 'psk',
-                           value: wirelessSecuritySetting.psk || '',
-                           validate: this._validateWpaPsk, password: true });
-            break;
-        case 'none': // static WEP
-            secrets.push({ label: _("Key: "), key: 'wep-key' + wirelessSecuritySetting.wep_tx_keyidx,
-                           value: wirelessSecuritySetting.get_wep_key(wirelessSecuritySetting.wep_tx_keyidx) || '',
-                           wep_key_type: wirelessSecuritySetting.wep_key_type,
-                           validate: this._validateStaticWep, password: true });
-            break;
-        case 'ieee8021x':
-            if (wirelessSecuritySetting.auth_alg == 'leap') // Cisco LEAP
-                secrets.push({ label: _("Password: "), key: 'leap-password',
-                               value: wirelessSecuritySetting.leap_password || '', password: true });
-            else // Dynamic (IEEE 802.1x) WEP
-                this._get8021xSecrets(secrets);
-            break;
-        case 'wpa-eap':
-            this._get8021xSecrets(secrets);
-            break;
-        default:
-            log('Invalid wireless key management: ' + wirelessSecuritySetting.key_mgmt);
-        }
-    },
-
-    _get8021xSecrets: function(secrets) {
-        let ieee8021xSetting = this._connection.get_setting_802_1x();
-        let phase2method;
-
-        switch (ieee8021xSetting.get_eap_method(0)) {
-        case 'md5':
-        case 'leap':
-        case 'ttls':
-        case 'peap':
-            // TTLS and PEAP are actually much more complicated, but this complication
-            // is not visible here since we only care about phase2 authentication
-            // (and don't even care of which one)
-            secrets.push({ label: _("Username: "), key: null,
-                           value: ieee8021xSetting.identity || '', password: false });
-            secrets.push({ label: _("Password: "), key: 'password',
-                           value: ieee8021xSetting.password || '', password: true });
-            break;
-        case 'tls':
-            secrets.push({ label: _("Identity: "), key: null,
-                           value: ieee8021xSetting.identity || '', password: false });
-            secrets.push({ label: _("Private key password: "), key: 'private-key-password',
-                           value: ieee8021xSetting.private_key_password || '', password: true });
-            break;
-        default:
-            log('Invalid EAP/IEEE802.1x method: ' + ieee8021xSetting.get_eap_method(0));
-        }
-    },
-
-    _getPPPoESecrets: function(secrets) {
-        let pppoeSetting = this._connection.get_setting_pppoe();
-        secrets.push({ label: _("Username: "), key: 'username',
-                       value: pppoeSetting.username || '', password: false });
-        secrets.push({ label: _("Service: "), key: 'service',
-                       value: pppoeSetting.service || '', password: false });
-        secrets.push({ label: _("Password: "), key: 'password',
-                       value: pppoeSetting.password || '', password: true });
-    },
-
-    _getMobileSecrets: function(secrets, connectionType) {
-        let setting;
-        if (connectionType == 'bluetooth')
-            setting = this._connection.get_setting_cdma() || this._connection.get_setting_gsm();
-        else
-            setting = this._connection.get_setting_by_name(connectionType);
-        secrets.push({ label: _("Password: "), key: 'password',
-                       value: setting.value || '', password: true });
-    },
-
-    _getContent: function() {
-        let connectionSetting = this._connection.get_setting_connection();
-        let connectionType = connectionSetting.get_connection_type();
-        let wirelessSetting;
-        let ssid;
-
-        let content = { };
-        content.secrets = [ ];
-
-        switch (connectionType) {
-        case '802-11-wireless':
-            wirelessSetting = this._connection.get_setting_wireless();
-            ssid = NetworkManager.utils_ssid_to_utf8(wirelessSetting.get_ssid());
-            content.title = _("Authentication required by wireless network");
-            content.message = _("Passwords or encryption keys are required to access the wireless network '%s'.").format(ssid);
-            this._getWirelessSecrets(content.secrets, wirelessSetting);
-            break;
-        case '802-3-ethernet':
-            content.title = _("Wired 802.1X authentication");
-            content.message = null;
-            content.secrets.push({ label: _("Network name: "), key: null,
-                                   value: connectionSetting.get_id(), password: false });
-            this._get8021xSecrets(content.secrets);
-            break;
-        case 'pppoe':
-            content.title = _("DSL authentication");
-            content.message = null;
-            this._getPPPoESecrets(content.secrets);
-            break;
-        case 'gsm':
-            if (this._hints.indexOf('pin') != -1) {
-                let gsmSetting = this._connection.get_setting_gsm();
-                content.title = _("PIN code required");
-                content.message = _("PIN code is needed for the mobile broadband device");
-                content.secrets.push({ label: _("PIN: "), key: 'pin',
-                                       value: gsmSetting.pin || '', password: true });
-            }
-            // fall through
-        case 'cdma':
-        case 'bluetooth':
-            content.title = _("Mobile broadband network password");
-            content.message = _("A password is required to connect to '%s'.").format(connectionSetting.get_id());
-            this._getMobileSecrets(content.secrets, connectionType);
-            break;
-        default:
-            log('Invalid connection type: ' + connectionType);
-        };
-
-        return content;
-    }
-});
-
-const VPNRequestHandler = new Lang.Class({
-    Name: 'VPNRequestHandler',
-
-    _init: function(agent, requestId, authHelper, serviceType, connection, hints, flags) {
-        this._agent = agent;
-        this._requestId = requestId;
-        this._connection = connection;
-        this._pluginOutBuffer = [];
-        this._title = null;
-        this._description = null;
-        this._content = [ ];
-        this._shellDialog = null;
-
-        let connectionSetting = connection.get_setting_connection();
-
-        let argv = [ authHelper.fileName,
-                     '-u', connectionSetting.uuid,
-                     '-n', connectionSetting.id,
-                     '-s', serviceType
-                   ];
-        if (authHelper.externalUIMode)
-            argv.push('--external-ui-mode');
-        if (flags & NMClient.SecretAgentGetSecretsFlags.ALLOW_INTERACTION)
-            argv.push('-i');
-        if (flags & NMClient.SecretAgentGetSecretsFlags.REQUEST_NEW)
-            argv.push('-r');
-
-        this._newStylePlugin = authHelper.externalUIMode;
-
-        try {
-            let [success, pid, stdin, stdout, stderr] =
-                GLib.spawn_async_with_pipes(null, /* pwd */
-                                            argv,
-                                            null, /* envp */
-                                            GLib.SpawnFlags.DO_NOT_REAP_CHILD,
-                                            null /* child_setup */);
-
-            this._childPid = pid;
-            this._stdin = new Gio.UnixOutputStream({ fd: stdin, close_fd: true });
-            this._stdout = new Gio.UnixInputStream({ fd: stdout, close_fd: true });
-            // We need this one too, even if don't actually care of what the process
-            // has to say on stderr, because otherwise the fd opened by g_spawn_async_with_pipes
-            // is kept open indefinitely
-            let stderrStream = new Gio.UnixInputStream({ fd: stderr, close_fd: true });
-            stderrStream.close(null);
-            this._dataStdout = new Gio.DataInputStream({ base_stream: this._stdout });
-
-            if (this._newStylePlugin)
-                this._readStdoutNewStyle();
-            else
-                this._readStdoutOldStyle();
-
-            this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid,
-                                                    Lang.bind(this, this._vpnChildFinished));
-
-            this._writeConnection();
-        } catch(e) {
-            logError(e, 'error while spawning VPN auth helper');
-
-            this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
-        }
-    },
-
-    cancel: function() {
-        if (this._newStylePlugin && this._shellDialog) {
-            this._shellDialog.close(global.get_current_time());
-            this._shellDialog.destroy();
-        } else {
-            try {
-                this._stdin.write('QUIT\n\n', null);
-            } catch(e) { /* ignore broken pipe errors */ }
-        }
-
-        this.destroy();
-    },
-
-    destroy: function() {
-        if (this._destroyed)
-            return;
-
-        GLib.source_remove(this._childWatch);
-
-        this._stdin.close(null);
-        // Stdout is closed when we finish reading from it
-
-        this._destroyed = true;
-    },
-
-    _vpnChildFinished: function(pid, status, requestObj) {
-        if (this._newStylePlugin) {
-            // For new style plugin, all work is done in the async reading functions
-            // Just reap the process here
-            return;
-        }
-
-        let [exited, exitStatus] = Shell.util_wifexited(status);
-
-        if (exited) {
-            if (exitStatus != 0)
-                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
-            else
-                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
-        } else
-            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
-
-        this.destroy();
-    },
-
-    _vpnChildProcessLineOldStyle: function(line) {
-        if (this._previousLine != undefined) {
-            // Two consecutive newlines mean that the child should be closed
-            // (the actual newlines are eaten by Gio.DataInputStream)
-            // Send a termination message
-            if (line == '' && this._previousLine == '') {
-                try {
-                    this._stdin.write('QUIT\n\n', null);
-                } catch(e) { /* ignore broken pipe errors */ }
-            } else {
-                this._agent.set_password(this._requestId, this._previousLine, line);
-                this._previousLine = undefined;
-            }
-        } else {
-            this._previousLine = line;
-        }
-    },
-
-    _readStdoutOldStyle: function() {
-        this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
-            let [line, len] = this._dataStdout.read_line_finish_utf8(result);
-
-            if (line == null) {
-                // end of file
-                this._stdout.close(null);
-                return;
-            }
-
-            this._vpnChildProcessLineOldStyle(line);
-
-            // try to read more!
-            this._readStdoutOldStyle();
-        }));
-    },
-
-    _readStdoutNewStyle: function() {
-        this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(stream, result) {
-            let cnt = this._dataStdout.fill_finish(result);
-
-            if (cnt == 0) {
-                // end of file
-                this._showNewStyleDialog();
-
-                this._stdout.close(null);
-                return;
-            }
-
-            // Try to read more
-            this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size());
-            this._readStdoutNewStyle();
-        }));
-    },
-
-    _showNewStyleDialog: function() {
-        let keyfile = new GLib.KeyFile();
-        let contentOverride;
-
-        try {
-            let data = this._dataStdout.peek_buffer();
-            keyfile.load_from_data(data.toString(), data.length,
-                                   GLib.KeyFileFlags.NONE);
-
-            if (keyfile.get_integer(VPN_UI_GROUP, 'Version') != 2)
-                throw new Error('Invalid plugin keyfile version, is %d');
-
-            contentOverride = { title: keyfile.get_string(VPN_UI_GROUP, 'Title'),
-                                message: keyfile.get_string(VPN_UI_GROUP, 'Description'),
-                                secrets: [] };
-
-            let [groups, len] = keyfile.get_groups();
-            for (let i = 0; i < groups.length; i++) {
-                if (groups[i] == VPN_UI_GROUP)
-                    continue;
-
-                let value = keyfile.get_string(groups[i], 'Value');
-                let shouldAsk = keyfile.get_boolean(groups[i], 'ShouldAsk');
-
-                if (shouldAsk) {
-                    contentOverride.secrets.push({ label: keyfile.get_string(groups[i], 'Label'),
-                                                   key: groups[i],
-                                                   value: value,
-                                                   password: keyfile.get_boolean(groups[i], 'IsSecret')
-                                                 });
-                } else {
-                    if (!value.length) // Ignore empty secrets
-                        continue;
-
-                    this._agent.set_password(this._requestId, groups[i], value);
-                }
-            }
-        } catch(e) {
-            logError(e, 'error while reading VPN plugin output keyfile');
-
-            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
-            return;
-        }
-
-        if (contentOverride.secrets.length) {
-            // Only show the dialog if we actually have something to ask
-            this._shellDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], contentOverride);
-            this._shellDialog.open(global.get_current_time());
-        } else {
-            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
-        }
-    },
-
-    _writeConnection: function() {
-        let vpnSetting = this._connection.get_setting_vpn();
-
-        try {
-            vpnSetting.foreach_data_item(Lang.bind(this, function(key, value) {
-                this._stdin.write('DATA_KEY=' + key + '\n', null);
-                this._stdin.write('DATA_VAL=' + (value || '') + '\n\n', null);
-            }));
-            vpnSetting.foreach_secret(Lang.bind(this, function(key, value) {
-                this._stdin.write('SECRET_KEY=' + key + '\n', null);
-                this._stdin.write('SECRET_VAL=' + (value || '') + '\n\n', null);
-            }));
-            this._stdin.write('DONE\n\n', null);
-        } catch(e) {
-            logError(e, 'internal error while writing connection to helper');
-
-            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
-        }
-    },
-});
-
-const NetworkAgent = new Lang.Class({
-    Name: 'NetworkAgent',
-
-    _init: function() {
-        this._native = new Shell.NetworkAgent({ auto_register: true,
-                                                identifier: 'org.gnome.Shell.NetworkAgent' });
-
-        this._dialogs = { };
-        this._vpnRequests = { };
-
-        this._native.connect('new-request', Lang.bind(this, this._newRequest));
-        this._native.connect('cancel-request', Lang.bind(this, this._cancelRequest));
-    },
-
-    _newRequest:  function(agent, requestId, connection, settingName, hints, flags) {
-        if (settingName == 'vpn') {
-            this._vpnRequest(requestId, connection, hints, flags);
-            return;
-        }
-
-        let dialog = new NetworkSecretDialog(agent, requestId, connection, settingName, hints);
-        dialog.connect('destroy', Lang.bind(this, function() {
-            delete this._dialogs[requestId];
-        }));
-        this._dialogs[requestId] = dialog;
-        dialog.open(global.get_current_time());
-    },
-
-    _cancelRequest: function(agent, requestId) {
-        if (this._dialogs[requestId]) {
-            this._dialogs[requestId].close(global.get_current_time());
-            this._dialogs[requestId].destroy();
-            delete this._dialogs[requestId];
-        } else if (this._vpnRequests[requestId]) {
-            this._vpnRequests[requestId].cancel();
-            delete this._vpnRequests[requestId];
-        }
-    },
-
-    _vpnRequest: function(requestId, connection, hints, flags) {
-        let vpnSetting = connection.get_setting_vpn();
-        let serviceType = vpnSetting.service_type;
-
-        this._buildVPNServiceCache();
-
-        let binary = this._vpnBinaries[serviceType];
-        if (!binary) {
-            log('Invalid VPN service type (cannot find authentication binary)');
-
-            /* cancel the auth process */
-            this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
-            return;
-        }
-
-        this._vpnRequests[requestId] = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags);
-    },
-
-    _buildVPNServiceCache: function() {
-        if (this._vpnCacheBuilt)
-            return;
-
-        this._vpnCacheBuilt = true;
-        this._vpnBinaries = { };
-
-        let dir = Gio.file_new_for_path(GLib.build_filenamev([Config.SYSCONFDIR, 'NetworkManager/VPN']));
-        try {
-            let fileEnum = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
-            let info;
-
-            while ((info = fileEnum.next_file(null))) {
-                let name = info.get_name();
-                if (name.substr(-5) != '.name')
-                    continue;
-
-                try {
-                    let keyfile = new GLib.KeyFile();
-                    keyfile.load_from_file(dir.get_child(name).get_path(), GLib.KeyFileFlags.NONE);
-                    let service = keyfile.get_string('VPN Connection', 'service');
-                    let binary = keyfile.get_string('GNOME', 'auth-dialog');
-                    let externalUIMode = false;
-                    try {
-                        externalUIMode = keyfile.get_boolean('GNOME', 'supports-external-ui-mode');
-                    } catch(e) { } // ignore errors if key does not exist
-                    let path = binary;
-                    if (!GLib.path_is_absolute(path)) {
-                        path = GLib.build_filenamev([Config.LIBEXECDIR, path]);
-                    }
-
-                    if (GLib.file_test(path, GLib.FileTest.IS_EXECUTABLE))
-                        this._vpnBinaries[service] = { fileName: path, externalUIMode: externalUIMode };
-                    else
-                        throw new Error('VPN plugin at %s is not executable'.format(path));
-                } catch(e) {
-                    log('Error \'%s\' while processing VPN keyfile \'%s\''.
-                        format(e.message, dir.get_child(name).get_path()));
-                    continue;
-                }
-            }
-        } catch(e) {
-            logError(e, 'error while enumerating VPN auth helpers');
-        }
-    }
-});
diff --git a/js/ui/polkitAuthenticationAgent.js b/js/ui/polkitAuthenticationAgent.js
deleted file mode 100644 (file)
index 92e4c6a..0000000
+++ /dev/null
@@ -1,403 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/*
- * Copyright 2010 Red Hat, Inc
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2, or (at your option)
- * any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
- * 02111-1307, USA.
-*
- * Author: David Zeuthen <davidz@redhat.com>
- */
-
-const Lang = imports.lang;
-const Signals = imports.signals;
-const Shell = imports.gi.Shell;
-const AccountsService = imports.gi.AccountsService;
-const Clutter = imports.gi.Clutter;
-const St = imports.gi.St;
-const Pango = imports.gi.Pango;
-const GLib = imports.gi.GLib;
-const Gio = imports.gi.Gio;
-const Mainloop = imports.mainloop;
-const Polkit = imports.gi.Polkit;
-const PolkitAgent = imports.gi.PolkitAgent;
-
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
-const UserMenu = imports.ui.userMenu;
-
-const DIALOG_ICON_SIZE = 48;
-
-const AuthenticationDialog = new Lang.Class({
-    Name: 'AuthenticationDialog',
-    Extends: ModalDialog.ModalDialog,
-
-    _init: function(actionId, message, cookie, userNames) {
-        this.parent({ styleClass: 'prompt-dialog' });
-
-        this.actionId = actionId;
-        this.message = message;
-        this.userNames = userNames;
-        this._wasDismissed = false;
-        this._completed = false;
-
-        let mainContentBox = new St.BoxLayout({ style_class: 'prompt-dialog-main-layout',
-                                                vertical: false });
-        this.contentLayout.add(mainContentBox,
-                               { x_fill: true,
-                                 y_fill: true });
-
-        let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
-        mainContentBox.add(icon,
-                           { x_fill:  true,
-                             y_fill:  false,
-                             x_align: St.Align.END,
-                             y_align: St.Align.START });
-
-        let messageBox = new St.BoxLayout({ style_class: 'prompt-dialog-message-layout',
-                                            vertical: true });
-        mainContentBox.add(messageBox,
-                           { y_align: St.Align.START });
-
-        this._subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
-                                            text: _("Authentication Required") });
-
-        messageBox.add(this._subjectLabel,
-                       { y_fill:  false,
-                         y_align: St.Align.START });
-
-        this._descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
-                                                text: message });
-        this._descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
-        this._descriptionLabel.clutter_text.line_wrap = true;
-
-        messageBox.add(this._descriptionLabel,
-                       { y_fill:  true,
-                         y_align: St.Align.START });
-
-        if (userNames.length > 1) {
-            log('polkitAuthenticationAgent: Received ' + userNames.length +
-                ' identities that can be used for authentication. Only ' +
-                'considering one.');
-        }
-
-        let userName = GLib.get_user_name();
-        if (userNames.indexOf(userName) < 0)
-            userName = 'root';
-        if (userNames.indexOf(userName) < 0)
-            userName = userNames[0];
-
-        this._user = AccountsService.UserManager.get_default().get_user(userName);
-        let userRealName = this._user.get_real_name()
-        this._userLoadedId = this._user.connect('notify::is_loaded',
-                                                Lang.bind(this, this._onUserChanged));
-        this._userChangedId = this._user.connect('changed',
-                                                 Lang.bind(this, this._onUserChanged));
-
-        // Special case 'root'
-        let userIsRoot = false;
-        if (userName == 'root') {
-            userIsRoot = true;
-            userRealName = _("Administrator");
-        }
-
-        if (userIsRoot) {
-            let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-root-label',
-                                            text: userRealName }));
-            messageBox.add(userLabel);
-        } else {
-            let userBox = new St.BoxLayout({ style_class: 'polkit-dialog-user-layout',
-                                             vertical: false });
-            messageBox.add(userBox);
-            this._userAvatar = new UserMenu.UserAvatarWidget(this._user,
-                                                             { iconSize: DIALOG_ICON_SIZE,
-                                                               styleClass: 'polkit-dialog-user-icon' });
-            this._userAvatar.actor.hide();
-            userBox.add(this._userAvatar.actor,
-                        { x_fill:  true,
-                          y_fill:  false,
-                          x_align: St.Align.END,
-                          y_align: St.Align.START });
-            let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-label',
-                                            text: userRealName }));
-            userBox.add(userLabel,
-                        { x_fill:  true,
-                          y_fill:  false,
-                          x_align: St.Align.END,
-                          y_align: St.Align.MIDDLE });
-        }
-
-        this._onUserChanged();
-
-        this._passwordBox = new St.BoxLayout({ vertical: false, style_class: 'prompt-dialog-password-box' });
-        messageBox.add(this._passwordBox);
-        this._passwordLabel = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
-        this._passwordBox.add(this._passwordLabel, { y_fill: false, y_align: St.Align.MIDDLE });
-        this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
-                                             text: "",
-                                             can_focus: true});
-        ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
-        this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivate));
-        this._passwordBox.add(this._passwordEntry,
-                              { expand: true });
-        this.setInitialKeyFocus(this._passwordEntry);
-        this._passwordBox.hide();
-
-        this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label' });
-        this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
-        this._errorMessageLabel.clutter_text.line_wrap = true;
-        messageBox.add(this._errorMessageLabel);
-        this._errorMessageLabel.hide();
-
-        this._infoMessageLabel = new St.Label({ style_class: 'prompt-dialog-info-label' });
-        this._infoMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
-        this._infoMessageLabel.clutter_text.line_wrap = true;
-        messageBox.add(this._infoMessageLabel);
-        this._infoMessageLabel.hide();
-
-        /* text is intentionally non-blank otherwise the height is not the same as for
-         * infoMessage and errorMessageLabel - but it is still invisible because
-         * gnome-shell.css sets the color to be transparent
-         */
-        this._nullMessageLabel = new St.Label({ style_class: 'prompt-dialog-null-label',
-                                                text: 'abc'});
-        this._nullMessageLabel.add_style_class_name('hidden');
-        this._nullMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
-        this._nullMessageLabel.clutter_text.line_wrap = true;
-        messageBox.add(this._nullMessageLabel);
-        this._nullMessageLabel.show();
-
-        this.setButtons([{ label: _("Cancel"),
-                           action: Lang.bind(this, this.cancel),
-                           key:    Clutter.Escape
-                         },
-                         { label:  _("Authenticate"),
-                           action: Lang.bind(this, this._onAuthenticateButtonPressed),
-                           default: true
-                         }]);
-
-        this._doneEmitted = false;
-
-        this._identityToAuth = Polkit.UnixUser.new_for_name(userName);
-        this._cookie = cookie;
-
-        this._session = new PolkitAgent.Session({ identity: this._identityToAuth,
-                                                  cookie: this._cookie });
-        this._session.connect('completed', Lang.bind(this, this._onSessionCompleted));
-        this._session.connect('request', Lang.bind(this, this._onSessionRequest));
-        this._session.connect('show-error', Lang.bind(this, this._onSessionShowError));
-        this._session.connect('show-info', Lang.bind(this, this._onSessionShowInfo));
-    },
-
-    startAuthentication: function() {
-        this._session.initiate();
-    },
-
-    _ensureOpen: function() {
-        // NOTE: ModalDialog.open() is safe to call if the dialog is
-        // already open - it just returns true without side-effects
-        if (!this.open(global.get_current_time())) {
-            // This can fail if e.g. unable to get input grab
-            //
-            // In an ideal world this wouldn't happen (because the
-            // Shell is in complete control of the session) but that's
-            // just not how things work right now.
-            //
-            // One way to make this happen is by running 'sleep 3;
-            // pkexec bash' and then opening a popup menu.
-            //
-            // We could add retrying if this turns out to be a problem
-
-            log('polkitAuthenticationAgent: Failed to show modal dialog.' +
-                ' Dismissing authentication request for action-id ' + this.actionId +
-                ' cookie ' + this._cookie);
-            this._emitDone(false, true);
-        }
-    },
-
-    _emitDone: function(keepVisible, dismissed) {
-        if (!this._doneEmitted) {
-            this._doneEmitted = true;
-            this.emit('done', keepVisible, dismissed);
-        }
-    },
-
-    _onEntryActivate: function() {
-        let response = this._passwordEntry.get_text();
-        this._session.response(response);
-        // When the user responds, dismiss already shown info and
-        // error texts (if any)
-        this._errorMessageLabel.hide();
-        this._infoMessageLabel.hide();
-        this._nullMessageLabel.show();
-    },
-
-    _onAuthenticateButtonPressed: function() {
-        this._onEntryActivate();
-    },
-
-    _onSessionCompleted: function(session, gainedAuthorization) {
-        if (this._completed)
-            return;
-
-        this._completed = true;
-
-        if (!gainedAuthorization) {
-            /* Unless we are showing an existing error message from the PAM
-             * module (the PAM module could be reporting the authentication
-             * error providing authentication-method specific information),
-             * show "Sorry, that didn't work. Please try again."
-             */
-            if (!this._errorMessageLabel.visible && !this._wasDismissed) {
-                /* Translators: "that didn't work" refers to the fact that the
-                 * requested authentication was not gained; this can happen
-                 * because of an authentication error (like invalid password),
-                 * for instance. */
-                this._errorMessageLabel.set_text(_("Sorry, that didn\'t work. Please try again."));
-                this._errorMessageLabel.show();
-                this._infoMessageLabel.hide();
-                this._nullMessageLabel.hide();
-            }
-        }
-        this._emitDone(!gainedAuthorization, false);
-    },
-
-    _onSessionRequest: function(session, request, echo_on) {
-        // Cheap localization trick
-        if (request == 'Password:' || request == 'Password: ')
-            this._passwordLabel.set_text(_("Password:"));
-        else
-            this._passwordLabel.set_text(request);
-
-        if (echo_on)
-            this._passwordEntry.clutter_text.set_password_char('');
-        else
-            this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
-
-        this._passwordBox.show();
-        this._passwordEntry.set_text('');
-        this._passwordEntry.grab_key_focus();
-        this._ensureOpen();
-    },
-
-    _onSessionShowError: function(session, text) {
-        this._passwordEntry.set_text('');
-        this._errorMessageLabel.set_text(text);
-        this._errorMessageLabel.show();
-        this._infoMessageLabel.hide();
-        this._nullMessageLabel.hide();
-        this._ensureOpen();
-    },
-
-    _onSessionShowInfo: function(session, text) {
-        this._passwordEntry.set_text('');
-        this._infoMessageLabel.set_text(text);
-        this._infoMessageLabel.show();
-        this._errorMessageLabel.hide();
-        this._nullMessageLabel.hide();
-        this._ensureOpen();
-    },
-
-    destroySession: function() {
-        if (this._session) {
-            if (!this._completed)
-                this._session.cancel();
-            this._session = null;
-        }
-    },
-
-    _onUserChanged: function() {
-        if (this._user.is_loaded) {
-            this._userAvatar.update();
-            this._userAvatar.actor.show();
-        }
-    },
-
-    cancel: function() {
-        this._wasDismissed = true;
-        this.close(global.get_current_time());
-        this._emitDone(false, true);
-    },
-});
-Signals.addSignalMethods(AuthenticationDialog.prototype);
-
-const AuthenticationAgent = new Lang.Class({
-    Name: 'AuthenticationAgent',
-
-    _init: function() {
-        this._native = new Shell.PolkitAuthenticationAgent();
-        this._native.connect('initiate', Lang.bind(this, this._onInitiate));
-        this._native.connect('cancel', Lang.bind(this, this._onCancel));
-        this._currentDialog = null;
-        this._isCompleting = false;
-    },
-
-    _onInitiate: function(nativeAgent, actionId, message, iconName, cookie, userNames) {
-        this._currentDialog = new AuthenticationDialog(actionId, message, cookie, userNames);
-
-        // We actually don't want to open the dialog until we know for
-        // sure that we're going to interact with the user. For
-        // example, if the password for the identity to auth is blank
-        // (which it will be on a live CD) then there will be no
-        // conversation at all... of course, we don't *know* that
-        // until we actually try it.
-        //
-        // See https://bugzilla.gnome.org/show_bug.cgi?id=643062 for more
-        // discussion.
-
-        this._currentDialog.connect('done', Lang.bind(this, this._onDialogDone));
-        this._currentDialog.startAuthentication();
-    },
-
-    _onCancel: function(nativeAgent) {
-        this._completeRequest(false, false);
-    },
-
-    _onDialogDone: function(dialog, keepVisible, dismissed) {
-        this._completeRequest(keepVisible, dismissed);
-    },
-
-    _reallyCompleteRequest: function(dismissed) {
-        this._currentDialog.close();
-        this._currentDialog.destroySession();
-        this._currentDialog = null;
-        this._isCompleting = false;
-
-        this._native.complete(dismissed)
-    },
-
-    _completeRequest: function(keepVisible, wasDismissed) {
-        if (this._isCompleting)
-            return;
-
-        this._isCompleting = true;
-
-        if (keepVisible) {
-            // Give the user 2 seconds to read 'Authentication Failure' before
-            // dismissing the dialog
-            Mainloop.timeout_add(2000,
-                                 Lang.bind(this,
-                                           function() {
-                                               this._reallyCompleteRequest(wasDismissed);
-                                               return false;
-                                           }));
-        } else {
-            this._reallyCompleteRequest(wasDismissed);
-        }
-    }
-});
-
-function init() {
-    let agent = new AuthenticationAgent();
-}
index 09c05608ec3ece03ec062db364005979da409f8e..572864312a8b2ee985c103e24a9dd52ec4fb9168 100644 (file)
@@ -17,8 +17,8 @@ const _modes = {
              hasRunDialog: false,
              hasWorkspaces: false,
              hasWindows: false,
-             createSession: Main.createGDMSession,
              createUnlockDialog: Main.createGDMLoginDialog,
+             components: [],
              panel: {
                  left: [],
                  center: ['dateMenu'],
@@ -36,6 +36,7 @@ const _modes = {
         hasRunDialog: false,
         hasWorkspaces: false,
         hasWindows: false,
+        components: ['networkAgent', 'polkitAgent', 'telepathyClient'],
         panel: {
             left: ['userMenu'],
             center: [],
@@ -50,7 +51,7 @@ const _modes = {
                        allowKeybindingsWhenModal: false,
                        hasRunDialog: false,
                        hasWorkspaces: false,
-                       createSession: Main.createInitialSetupSession,
+                       components: ['keyring'],
                        panel: {
                            left: [],
                            center: ['dateMenu'],
@@ -66,8 +67,9 @@ const _modes = {
               hasRunDialog: true,
               hasWorkspaces: true,
               hasWindows: true,
-              createSession: Main.createUserSession,
               createUnlockDialog: Main.createSessionUnlockDialog,
+              components: ['networkAgent', 'polkitAgent', 'telepathyClient',
+                           'keyring', 'recorder', 'autorunManager', 'automountManager'],
               panel: {
                   left: ['activities', 'appMenu'],
                   center: ['dateMenu'],
@@ -112,8 +114,6 @@ const SessionMode = new Lang.Class({
         let params = _modes[this.currentMode];
         params = Params.parse(params, _modes[DEFAULT_MODE]);
 
-        this._createSession = params.createSession;
-        delete params.createSession;
         this._createUnlockDialog = params.createUnlockDialog;
         delete params.createUnlockDialog;
 
@@ -121,11 +121,6 @@ const SessionMode = new Lang.Class({
         this.emit('updated');
     },
 
-    createSession: function() {
-        if (this._createSession)
-            this._createSession();
-    },
-
     createUnlockDialog: function() {
         if (this._createUnlockDialog)
             return this._createUnlockDialog.apply(this, arguments);
diff --git a/js/ui/telepathyClient.js b/js/ui/telepathyClient.js
deleted file mode 100644 (file)
index 08218d7..0000000
+++ /dev/null
@@ -1,1401 +0,0 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const Lang = imports.lang;
-const Mainloop = imports.mainloop;
-const Shell = imports.gi.Shell;
-const Signals = imports.signals;
-const St = imports.gi.St;
-const Tpl = imports.gi.TelepathyLogger;
-const Tp = imports.gi.TelepathyGLib;
-
-const History = imports.misc.history;
-const Params = imports.misc.params;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-
-
-// See Notification.appendMessage
-const SCROLLBACK_IMMEDIATE_TIME = 60; // 1 minute
-const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
-const SCROLLBACK_RECENT_LENGTH = 20;
-const SCROLLBACK_IDLE_LENGTH = 5;
-
-// See Source._displayPendingMessages
-const SCROLLBACK_HISTORY_LINES = 10;
-
-// See Notification._onEntryChanged
-const COMPOSING_STOP_TIMEOUT = 5;
-
-const NotificationDirection = {
-    SENT: 'chat-sent',
-    RECEIVED: 'chat-received'
-};
-
-function makeMessageFromTpMessage(tpMessage, direction) {
-    let [text, flags] = tpMessage.to_text();
-
-    let timestamp = tpMessage.get_sent_timestamp();
-    if (timestamp == 0)
-        timestamp = tpMessage.get_received_timestamp();
-
-    return {
-        messageType: tpMessage.get_message_type(),
-        text: text,
-        sender: tpMessage.sender.alias,
-        timestamp: timestamp,
-        direction: direction
-    };
-}
-
-
-function makeMessageFromTplEvent(event) {
-    let sent = event.get_sender().get_entity_type() == Tpl.EntityType.SELF;
-    let direction = sent ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
-
-    return {
-        messageType: event.get_message_type(),
-        text: event.get_message(),
-        sender: event.get_sender().get_alias(),
-        timestamp: event.get_timestamp(),
-        direction: direction
-    };
-}
-
-const Client = new Lang.Class({
-    Name: 'Client',
-
-    _init : function() {
-        // channel path -> ChatSource
-        this._chatSources = {};
-        this._chatState = Tp.ChannelChatState.ACTIVE;
-
-        // account path -> AccountNotification
-        this._accountNotifications = {};
-
-        // Define features we want
-        this._accountManager = Tp.AccountManager.dup();
-        let factory = this._accountManager.get_factory();
-        factory.add_account_features([Tp.Account.get_feature_quark_connection()]);
-        factory.add_connection_features([Tp.Connection.get_feature_quark_contact_list()]);
-        factory.add_channel_features([Tp.Channel.get_feature_quark_contacts()]);
-        factory.add_contact_features([Tp.ContactFeature.ALIAS,
-                                      Tp.ContactFeature.AVATAR_DATA,
-                                      Tp.ContactFeature.PRESENCE,
-                                      Tp.ContactFeature.SUBSCRIPTION_STATES]);
-
-        // Set up a SimpleObserver, which will call _observeChannels whenever a
-        // channel matching its filters is detected.
-        // The second argument, recover, means _observeChannels will be run
-        // for any existing channel as well.
-        this._tpClient = new Shell.TpClient({ 'account-manager': this._accountManager,
-                                              'name': 'GnomeShell',
-                                              'uniquify-name': true })
-        this._tpClient.set_observe_channels_func(
-            Lang.bind(this, this._observeChannels));
-        this._tpClient.set_approve_channels_func(
-            Lang.bind(this, this._approveChannels));
-        this._tpClient.set_handle_channels_func(
-            Lang.bind(this, this._handleChannels));
-
-        // Workaround for gjs not supporting GPtrArray in signals.
-        // See BGO bug #653941 for context.
-        this._tpClient.set_contact_list_changed_func(
-            Lang.bind(this, this._contactListChanged));
-
-        // Allow other clients (such as Empathy) to pre-empt our channels if
-        // needed
-        this._tpClient.set_delegated_channels_callback(
-            Lang.bind(this, this._delegatedChannelsCb));
-
-        try {
-            this._tpClient.register();
-        } catch (e) {
-            throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
-        }
-
-        // Watch subscription requests and connection errors
-        this._subscriptionSource = null;
-        this._accountSource = null;
-
-        this._accountManager.connect('account-validity-changed',
-            Lang.bind(this, this._accountValidityChanged));
-
-        this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared));
-    },
-
-    _observeChannels: function(observer, account, conn, channels,
-                               dispatchOp, requests, context) {
-        let len = channels.length;
-        for (let i = 0; i < len; i++) {
-            let channel = channels[i];
-            let [targetHandle, targetHandleType] = channel.get_handle();
-
-            if (channel.get_invalidated())
-              continue;
-
-            /* Only observe contact text channels */
-            if ((!(channel instanceof Tp.TextChannel)) ||
-               targetHandleType != Tp.HandleType.CONTACT)
-               continue;
-
-            this._createChatSource(account, conn, channel, channel.get_target_contact());
-        }
-
-        context.accept();
-    },
-
-    _createChatSource: function(account, conn, channel, contact) {
-        if (this._chatSources[channel.get_object_path()])
-            return;
-
-        let source = new ChatSource(account, conn, channel, contact, this._tpClient);
-
-        this._chatSources[channel.get_object_path()] = source;
-        source.connect('destroy', Lang.bind(this,
-                       function() {
-                           if (this._tpClient.is_handling_channel(channel)) {
-                               // The chat box has been destroyed so it can't
-                               // handle the channel any more.
-                               channel.close_async(function(src, result) {
-                                   channel.close_finish(result);
-                               });
-                           }
-
-                           delete this._chatSources[channel.get_object_path()];
-                       }));
-    },
-
-    _handleChannels: function(handler, account, conn, channels,
-                              requests, user_action_time, context) {
-        this._handlingChannels(account, conn, channels, true);
-        context.accept();
-    },
-
-    _handlingChannels: function(account, conn, channels, notify) {
-        let len = channels.length;
-        for (let i = 0; i < len; i++) {
-            let channel = channels[i];
-
-            // We can only handle text channel, so close any other channel
-            if (!(channel instanceof Tp.TextChannel)) {
-                channel.close_async(null);
-                continue;
-            }
-
-            if (channel.get_invalidated())
-              continue;
-
-            // 'notify' will be true when coming from an actual HandleChannels
-            // call, and not when from a successful Claim call. The point is
-            // we don't want to notify for a channel we just claimed which
-            // has no new messages (for example, a new channel which only has
-            // a delivery notification). We rely on _displayPendingMessages()
-            // and _messageReceived() to notify for new messages.
-
-            // But we should still notify from HandleChannels because the
-            // Telepathy spec states that handlers must foreground channels
-            // in HandleChannels calls which are already being handled.
-
-            if (notify && this._tpClient.is_handling_channel(channel)) {
-                // We are already handling the channel, display the source
-                let source = this._chatSources[channel.get_object_path()];
-                if (source)
-                    source.notify();
-            }
-        }
-    },
-
-    _displayRoomInvitation: function(conn, channel, dispatchOp, context) {
-        // We can only approve the rooms if we have been invited to it
-        let selfContact = channel.group_get_self_contact();
-        if (selfContact == null) {
-            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
-                                        message: 'Not invited to the room' }));
-            return;
-        }
-
-        let [invited, inviter, reason, msg] = channel.group_get_local_pending_contact_info(selfContact);
-        if (!invited) {
-            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
-                                        message: 'Not invited to the room' }));
-            return;
-        }
-
-        // FIXME: We don't have a 'chat room' icon (bgo #653737) use
-        // system-users for now as Empathy does.
-        let source = new ApproverSource(dispatchOp, _("Invitation"),
-                                        Gio.icon_new_for_string('system-users'));
-        Main.messageTray.add(source);
-
-        let notif = new RoomInviteNotification(source, dispatchOp, channel, inviter);
-        source.notify(notif);
-        context.accept();
-    },
-
-    _approveChannels: function(approver, account, conn, channels,
-                               dispatchOp, context) {
-        let channel = channels[0];
-        let chanType = channel.get_channel_type();
-
-        if (channel.get_invalidated()) {
-            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
-                                        message: 'Channel is invalidated' }));
-            return;
-        }
-
-        if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT)
-            this._approveTextChannel(account, conn, channel, dispatchOp, context);
-        else if (chanType == Tp.IFACE_CHANNEL_TYPE_CALL)
-            this._approveCall(account, conn, channel, dispatchOp, context);
-        else if (chanType == Tp.IFACE_CHANNEL_TYPE_FILE_TRANSFER)
-            this._approveFileTransfer(account, conn, channel, dispatchOp, context);
-        else
-            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
-                                        message: 'Unsupported channel type' }));
-    },
-
-    _approveTextChannel: function(account, conn, channel, dispatchOp, context) {
-        let [targetHandle, targetHandleType] = channel.get_handle();
-
-        if (targetHandleType == Tp.HandleType.CONTACT) {
-            // Approve private text channels right away as we are going to handle it
-            dispatchOp.claim_with_async(this._tpClient,
-                                        Lang.bind(this, function(dispatchOp, result) {
-                try {
-                    dispatchOp.claim_with_finish(result);
-                    this._handlingChannels(account, conn, [channel], false);
-                } catch (err) {
-                    throw new Error('Failed to Claim channel: ' + err);
-                }}));
-
-            context.accept();
-        } else {
-            this._displayRoomInvitation(conn, channel, dispatchOp, context);
-        }
-    },
-
-    _approveCall: function(account, conn, channel, dispatchOp, context) {
-        let isVideo = false;
-
-        let props = channel.borrow_immutable_properties();
-
-        if (props[Tp.PROP_CHANNEL_TYPE_CALL_INITIAL_VIDEO])
-          isVideo = true;
-
-        // We got the TpContact
-        let source = new ApproverSource(dispatchOp, _("Call"), isVideo ?
-                                        Gio.icon_new_for_string('camera-web') :
-                                        Gio.icon_new_for_string('audio-input-microphone'));
-        Main.messageTray.add(source);
-
-        let notif = new AudioVideoNotification(source, dispatchOp, channel,
-            channel.get_target_contact(), isVideo);
-        source.notify(notif);
-        context.accept();
-    },
-
-    _approveFileTransfer: function(account, conn, channel, dispatchOp, context) {
-        // Use the icon of the file being transferred
-        let gicon = Gio.content_type_get_icon(channel.get_mime_type());
-
-        // We got the TpContact
-        let source = new ApproverSource(dispatchOp, _("File Transfer"), gicon);
-        Main.messageTray.add(source);
-
-        let notif = new FileTransferNotification(source, dispatchOp, channel,
-            channel.get_target_contact());
-        source.notify(notif);
-        context.accept();
-    },
-
-    _delegatedChannelsCb: function(client, channels) {
-        // Nothing to do as we don't make a distinction between observed and
-        // handled channels.
-    },
-
-    _accountManagerPrepared: function(am, result) {
-        am.prepare_finish(result);
-
-        let accounts = am.get_valid_accounts();
-        for (let i = 0; i < accounts.length; i++) {
-            this._accountValidityChanged(am, accounts[i], true);
-        }
-    },
-
-    _accountValidityChanged: function(am, account, valid) {
-        if (!valid)
-            return;
-
-        // It would be better to connect to "status-changed" but we cannot.
-        // See discussion in https://bugzilla.gnome.org/show_bug.cgi?id=654159
-        account.connect("notify::connection-status",
-                        Lang.bind(this, this._accountConnectionStatusNotifyCb));
-
-        account.connect('notify::connection',
-                        Lang.bind(this, this._connectionChanged));
-        this._connectionChanged(account);
-    },
-
-    _connectionChanged: function(account) {
-        let conn = account.get_connection();
-        if (conn == null)
-            return;
-
-        this._tpClient.grab_contact_list_changed(conn);
-        if (conn.get_contact_list_state() == Tp.ContactListState.SUCCESS) {
-            this._contactListChanged(conn, conn.dup_contact_list(), []);
-        }
-    },
-
-    _contactListChanged: function(conn, added, removed) {
-        for (let i = 0; i < added.length; i++) {
-            let contact = added[i];
-
-            contact.connect('subscription-states-changed',
-                            Lang.bind(this, this._subscriptionStateChanged));
-            this._subscriptionStateChanged(contact);
-        }
-    },
-
-    _subscriptionStateChanged: function(contact) {
-        if (contact.get_publish_state() != Tp.SubscriptionState.ASK)
-            return;
-
-        /* Implicitly accept publish requests if contact is already subscribed */
-        if (contact.get_subscribe_state() == Tp.SubscriptionState.YES ||
-            contact.get_subscribe_state() == Tp.SubscriptionState.ASK) {
-
-            contact.authorize_publication_async(function(src, result) {
-                src.authorize_publication_finish(result)});
-
-            return;
-        }
-
-        /* Display notification to ask user to accept/reject request */
-        let source = this._ensureSubscriptionSource();
-
-        let notif = new SubscriptionRequestNotification(source, contact);
-        source.notify(notif);
-    },
-
-    _ensureSubscriptionSource: function() {
-        if (this._subscriptionSource == null) {
-            this._subscriptionSource = new MessageTray.Source(_("Subscription request"),
-                                                              'gtk-dialog-question');
-            Main.messageTray.add(this._subscriptionSource);
-            this._subscriptionSource.connect('destroy', Lang.bind(this, function () {
-                this._subscriptionSource = null;
-            }));
-        }
-
-        return this._subscriptionSource;
-    },
-
-    _accountConnectionStatusNotifyCb: function(account) {
-        let connectionError = account.connection_error;
-
-        if (account.connection_status != Tp.ConnectionStatus.DISCONNECTED ||
-            connectionError == Tp.error_get_dbus_name(Tp.Error.CANCELLED)) {
-            return;
-        }
-
-        let notif = this._accountNotifications[account.get_object_path()];
-        if (notif)
-            return;
-
-        /* Display notification that account failed to connect */
-        let source = this._ensureAccountSource();
-
-        notif = new AccountNotification(source, account, connectionError);
-        this._accountNotifications[account.get_object_path()] = notif;
-        notif.connect('destroy', Lang.bind(this, function() {
-            delete this._accountNotifications[account.get_object_path()];
-        }));
-        source.notify(notif);
-    },
-
-    _ensureAccountSource: function() {
-        if (this._accountSource == null) {
-            this._accountSource = new MessageTray.Source(_("Connection error"),
-                                                         'gtk-dialog-error');
-            Main.messageTray.add(this._accountSource);
-            this._accountSource.connect('destroy', Lang.bind(this, function () {
-                this._accountSource = null;
-            }));
-        }
-
-        return this._accountSource;
-    }
-});
-
-const ChatSource = new Lang.Class({
-    Name: 'ChatSource',
-    Extends: MessageTray.Source,
-
-    _init: function(account, conn, channel, contact, client) {
-        this._account = account;
-        this._contact = contact;
-        this._client = client;
-
-        this.parent(contact.get_alias());
-
-        this.isChat = true;
-        this._pendingMessages = [];
-
-        this._conn = conn;
-        this._channel = channel;
-        this._closedId = this._channel.connect('invalidated', Lang.bind(this, this._channelClosed));
-
-        this._notification = new ChatNotification(this);
-        this._notification.setUrgency(MessageTray.Urgency.HIGH);
-        this._notifyTimeoutId = 0;
-
-        // We ack messages when the user expands the new notification or views the summary
-        // notification, in which case the notification is also expanded.
-        this._notification.connect('expanded', Lang.bind(this, this._ackMessages));
-
-        this._presence = contact.get_presence_type();
-
-        this._sentId = this._channel.connect('message-sent', Lang.bind(this, this._messageSent));
-        this._receivedId = this._channel.connect('message-received', Lang.bind(this, this._messageReceived));
-        this._pendingId = this._channel.connect('pending-message-removed', Lang.bind(this, this._pendingRemoved));
-
-        this._notifyAliasId = this._contact.connect('notify::alias', Lang.bind(this, this._updateAlias));
-        this._notifyAvatarId = this._contact.connect('notify::avatar-file', Lang.bind(this, this._updateAvatarIcon));
-        this._presenceChangedId = this._contact.connect('presence-changed', Lang.bind(this, this._presenceChanged));
-
-        // Add ourselves as a source.
-        Main.messageTray.add(this);
-        this.pushNotification(this._notification);
-
-        this._getLogMessages();
-    },
-
-    _updateAlias: function() {
-        let oldAlias = this.title;
-        let newAlias = this._contact.get_alias();
-
-        if (oldAlias == newAlias)
-            return;
-
-        this.setTitle(newAlias);
-        this._notification.appendAliasChange(oldAlias, newAlias);
-    },
-
-    createIcon: function(size) {
-        this._iconBox = new St.Bin({ style_class: 'avatar-box' });
-        this._iconBox._size = size;
-        let textureCache = St.TextureCache.get_default();
-        let file = this._contact.get_avatar_file();
-
-        if (file) {
-            let uri = file.get_uri();
-            this._iconBox.child = textureCache.load_uri_async(uri, this._iconBox._size, this._iconBox._size);
-        } else {
-            this._iconBox.child = new St.Icon({ icon_name: 'avatar-default',
-                                                icon_size: this._iconBox._size });
-        }
-
-        return this._iconBox;
-    },
-
-    createSecondaryIcon: function() {
-        let iconBox = new St.Bin();
-        iconBox.child = new St.Icon({ style_class: 'secondary-icon' });
-        let presenceType = this._contact.get_presence_type();
-
-        switch (presenceType) {
-            case Tp.ConnectionPresenceType.AVAILABLE:
-                iconBox.child.icon_name = 'user-available';
-                break;
-            case Tp.ConnectionPresenceType.BUSY:
-                iconBox.child.icon_name = 'user-busy';
-                break;
-            case Tp.ConnectionPresenceType.OFFLINE:
-                iconBox.child.icon_name = 'user-offline';
-                break;
-            case Tp.ConnectionPresenceType.HIDDEN:
-                iconBox.child.icon_name = 'user-invisible';
-                break;
-            case Tp.ConnectionPresenceType.AWAY:
-                iconBox.child.icon_name = 'user-away';
-                break;
-            case Tp.ConnectionPresenceType.EXTENDED_AWAY:
-                iconBox.child.icon_name = 'user-idle';
-                break;
-            default:
-                iconBox.child.icon_name = 'user-offline';
-       }
-       return iconBox;
-    },
-
-    _updateAvatarIcon: function() {
-        this.iconUpdated();
-        this._notification.update(this._notification.title, null, { customContent: true });
-    },
-
-    open: function(notification) {
-          if (this._client.is_handling_channel(this._channel)) {
-              // We are handling the channel, try to pass it to Empathy
-              this._client.delegate_channels_async([this._channel], global.get_current_time(), '', null);
-          }
-          else {
-              // We are not the handler, just ask to present the channel
-              let dbus = Tp.DBusDaemon.dup();
-              let cd = Tp.ChannelDispatcher.new(dbus);
-
-              cd.present_channel_async(this._channel, global.get_current_time(), null);
-          }
-    },
-
-    _getLogMessages: function() {
-        let logManager = Tpl.LogManager.dup_singleton();
-        let entity = Tpl.Entity.new_from_tp_contact(this._contact, Tpl.EntityType.CONTACT);
-
-        logManager.get_filtered_events_async(this._account, entity,
-                                             Tpl.EventTypeMask.TEXT, SCROLLBACK_HISTORY_LINES,
-                                             null, Lang.bind(this, this._displayPendingMessages));
-    },
-
-    _displayPendingMessages: function(logManager, result) {
-        let [success, events] = logManager.get_filtered_events_finish(result);
-
-        let logMessages = events.map(makeMessageFromTplEvent);
-
-        let pendingTpMessages = this._channel.get_pending_messages();
-        let pendingMessages = [];
-
-        for (let i = 0; i < pendingTpMessages.length; i++) {
-            let message = pendingTpMessages[i];
-
-            if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
-                continue;
-
-            pendingMessages.push(makeMessageFromTpMessage(message, NotificationDirection.RECEIVED));
-
-            this._pendingMessages.push(message);
-        }
-
-        this.countUpdated();
-
-        let showTimestamp = false;
-
-        for (let i = 0; i < logMessages.length; i++) {
-            let logMessage = logMessages[i];
-            let isPending = false;
-
-            // Skip any log messages that are also in pendingMessages
-            for (let j = 0; j < pendingMessages.length; j++) {
-                let pending = pendingMessages[j];
-                if (logMessage.timestamp == pending.timestamp && logMessage.text == pending.text) {
-                    isPending = true;
-                    break;
-                }
-            }
-
-            if (!isPending) {
-                showTimestamp = true;
-                this._notification.appendMessage(logMessage, true, ['chat-log-message']);
-            }
-        }
-
-        if (showTimestamp)
-            this._notification.appendTimestamp();
-
-        for (let i = 0; i < pendingMessages.length; i++)
-            this._notification.appendMessage(pendingMessages[i], true);
-
-        if (pendingMessages.length > 0)
-            this.notify();
-    },
-
-    _channelClosed: function() {
-        this._channel.disconnect(this._closedId);
-        this._channel.disconnect(this._receivedId);
-        this._channel.disconnect(this._pendingId);
-        this._channel.disconnect(this._sentId);
-
-        this._contact.disconnect(this._notifyAliasId);
-        this._contact.disconnect(this._notifyAvatarId);
-        this._contact.disconnect(this._presenceChangedId);
-
-        this.destroy();
-    },
-
-    /* All messages are new messages for Telepathy sources */
-    get count() {
-        return this._pendingMessages.length;
-    },
-
-    get unseenCount() {
-        return this.count;
-    },
-
-    get countVisible() {
-        return this.count > 0;
-    },
-
-    _messageReceived: function(channel, message) {
-        if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
-            return;
-
-        this._pendingMessages.push(message);
-        this.countUpdated();
-
-        message = makeMessageFromTpMessage(message, NotificationDirection.RECEIVED);
-        this._notification.appendMessage(message);
-
-        // Wait a bit before notifying for the received message, a handler
-        // could ack it in the meantime.
-        if (this._notifyTimeoutId != 0)
-            Mainloop.source_remove(this._notifyTimeoutId);
-        this._notifyTimeoutId = Mainloop.timeout_add(500,
-            Lang.bind(this, this._notifyTimeout));
-    },
-
-    _notifyTimeout: function() {
-        if (this._pendingMessages.length != 0)
-            this.notify();
-
-        this._notifyTimeoutId = 0;
-
-        return false;
-    },
-
-    // This is called for both messages we send from
-    // our client and other clients as well.
-    _messageSent: function(channel, message, flags, token) {
-        message = makeMessageFromTpMessage(message, NotificationDirection.SENT);
-        this._notification.appendMessage(message);
-    },
-
-    notify: function() {
-        this.parent(this._notification);
-    },
-
-    respond: function(text) {
-        let type;
-        if (text.slice(0, 4) == '/me ') {
-            type = Tp.ChannelTextMessageType.ACTION;
-            text = text.slice(4);
-        } else {
-            type = Tp.ChannelTextMessageType.NORMAL;
-        }
-
-        let msg = Tp.ClientMessage.new_text(type, text);
-        this._channel.send_message_async(msg, 0, Lang.bind(this, function (src, result) {
-            this._channel.send_message_finish(result); 
-        }));
-    },
-
-    setChatState: function(state) {
-        // We don't want to send COMPOSING every time a letter is typed into
-        // the entry. We send the state only when it changes. Telepathy/Empathy
-        // might change it behind our back if the user is using both
-        // gnome-shell's entry and the Empathy conversation window. We could
-        // keep track of it with the ChatStateChanged signal but it is good
-        // enough right now.
-        if (state != this._chatState) {
-          this._chatState = state;
-          this._channel.set_chat_state_async(state, null);
-        }
-    },
-
-    _presenceChanged: function (contact, presence, status, message) {
-        let msg, title;
-
-        title = GLib.markup_escape_text(this.title, -1);
-
-        this._notification.update(this._notification.title, null, { customContent: true, secondaryIcon: this.createSecondaryIcon() });
-
-        if (message)
-            msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
-    },
-
-    _pendingRemoved: function(channel, message) {
-        let idx = this._pendingMessages.indexOf(message);
-
-        if (idx >= 0) {
-            this._pendingMessages.splice(idx, 1);
-            this.countUpdated();
-        }
-        else
-            throw new Error('Message not in our pending list: ' + message);
-    },
-
-    _ackMessages: function() {
-        // Don't clear our messages here, tp-glib will send a
-        // 'pending-message-removed' for each one.
-        this._channel.ack_all_pending_messages_async(Lang.bind(this, function(src, result) {
-            this._channel.ack_all_pending_messages_finish(result);}));
-    }
-});
-
-const ChatNotification = new Lang.Class({
-    Name: 'ChatNotification',
-    Extends: MessageTray.Notification,
-
-    _init: function(source) {
-        this.parent(source, source.title, null, { customContent: true, secondaryIcon: source.createSecondaryIcon() });
-        this.setResident(true);
-
-        this._responseEntry = new St.Entry({ style_class: 'chat-response',
-                                             can_focus: true });
-        this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
-        this._responseEntry.clutter_text.connect('text-changed', Lang.bind(this, this._onEntryChanged));
-        this.setActionArea(this._responseEntry);
-
-        this._oldMaxScrollAdjustment = 0;
-        this._createScrollArea();
-        this._lastGroup = null;
-        this._lastGroupActor = null;
-
-        this._scrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
-            let currentValue = adjustment.value + adjustment.page_size;
-            if (currentValue == this._oldMaxScrollAdjustment)
-                this.scrollTo(St.Side.BOTTOM);
-            this._oldMaxScrollAdjustment = adjustment.upper;
-        }));
-
-        this._inputHistory = new History.HistoryManager({ entry: this._responseEntry.clutter_text });
-
-        this._history = [];
-        this._timestampTimeoutId = 0;
-        this._composingTimeoutId = 0;
-    },
-
-    /**
-     * appendMessage:
-     * @message: An object with the properties:
-     *   text: the body of the message,
-     *   messageType: a #Tp.ChannelTextMessageType,
-     *   sender: the name of the sender,
-     *   timestamp: the time the message was sent
-     *   direction: a #NotificationDirection
-     * 
-     * @noTimestamp: Whether to add a timestamp. If %true, no timestamp
-     *   will be added, regardless of the difference since the
-     *   last timestamp
-     */
-    appendMessage: function(message, noTimestamp) {
-        let messageBody = GLib.markup_escape_text(message.text, -1);
-        let styles = [message.direction];
-
-        if (message.messageType == Tp.ChannelTextMessageType.ACTION) {
-            let senderAlias = GLib.markup_escape_text(message.sender, -1);
-            messageBody = '<i>%s</i> %s'.format(senderAlias, messageBody);
-            styles.push('chat-action');
-        }
-
-        if (message.direction == NotificationDirection.RECEIVED) {
-            this.update(this.source.title, messageBody, { customContent: true,
-                                                          bannerMarkup: true });
-        }
-
-        let group = (message.direction == NotificationDirection.RECEIVED ?
-                     'received' : 'sent');
-
-        this._append({ body: messageBody,
-                       group: group,
-                       styles: styles,
-                       timestamp: message.timestamp,
-                       noTimestamp: noTimestamp });
-    },
-
-    _filterMessages: function() {
-        if (this._history.length < 1)
-            return;
-
-        let lastMessageTime = this._history[0].time;
-        let currentTime = (Date.now() / 1000);
-
-        // Keep the scrollback from growing too long. If the most
-        // recent message (before the one we just added) is within
-        // SCROLLBACK_RECENT_TIME, we will keep
-        // SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
-        // we'll keep SCROLLBACK_IDLE_LENGTH messages.
-
-        let maxLength = (lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME) ?
-            SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
-
-        let filteredHistory = this._history.filter(function(item) { return item.realMessage });
-        if (filteredHistory.length > maxLength) {
-            let lastMessageToKeep = filteredHistory[maxLength];
-            let expired = this._history.splice(this._history.indexOf(lastMessageToKeep));
-            for (let i = 0; i < expired.length; i++)
-                expired[i].actor.destroy();
-        }
-
-        let groups = this._contentArea.get_children();
-        for (let i = 0; i < groups.length; i++) {
-            let group = groups[i];
-            if (group.get_n_children() == 0)
-                group.destroy();
-        }
-    },
-
-    /**
-     * _append:
-     * @props: An object with the properties:
-     *  body: The text of the message.
-     *  group: The group of the message, one of:
-     *         'received', 'sent', 'meta'.
-     *  styles: Style class names for the message to have.
-     *  timestamp: The timestamp of the message.
-     *  noTimestamp: suppress timestamp signal?
-     *  childProps: props to add the actor with.
-     */
-    _append: function(props) {
-        let currentTime = (Date.now() / 1000);
-        props = Params.parse(props, { body: null,
-                                      group: null,
-                                      styles: [],
-                                      timestamp: currentTime,
-                                      noTimestamp: false,
-                                      childProps: null });
-
-        // Reset the old message timeout
-        if (this._timestampTimeoutId)
-            Mainloop.source_remove(this._timestampTimeoutId);
-
-        let highlighter = new MessageTray.URLHighlighter(props.body,
-                                                         true,  // line wrap?
-                                                         true); // allow markup?
-
-        let body = highlighter.actor;
-
-        let styles = props.styles;
-        for (let i = 0; i < styles.length; i++)
-            body.add_style_class_name(styles[i]);
-
-        let group = props.group;
-        if (group != this._lastGroup) {
-            let style = 'chat-group-' + group;
-            this._lastGroup = group;
-            this._lastGroupActor = new St.BoxLayout({ style_class: style,
-                                                      vertical: true });
-            this.addActor(this._lastGroupActor);
-        }
-
-        this._lastGroupActor.add(body, props.childProps);
-
-        this.updated();
-
-        let timestamp = props.timestamp;
-        this._history.unshift({ actor: body, time: timestamp,
-                                realMessage: group != 'meta' });
-
-        if (!props.noTimestamp) {
-            if (timestamp < currentTime - SCROLLBACK_IMMEDIATE_TIME)
-                this.appendTimestamp();
-            else
-                // Schedule a new timestamp in SCROLLBACK_IMMEDIATE_TIME
-                // from the timestamp of the message.
-                this._timestampTimeoutId = Mainloop.timeout_add_seconds(
-                    SCROLLBACK_IMMEDIATE_TIME - (currentTime - timestamp),
-                    Lang.bind(this, this.appendTimestamp));
-        }
-
-        this._filterMessages();
-    },
-
-    _formatTimestamp: function(date) {
-        let now = new Date();
-
-        var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
-
-        let format;
-
-        // Show a week day and time if date is in the last week
-        if (daysAgo < 1 || (daysAgo < 7 && now.getDay() != date.getDay())) {
-            /* Translators: this is a time format string followed by a date.
-             If applicable, replace %X with a strftime format valid for your
-             locale, without seconds. */
-            // xgettext:no-c-format
-            format = _("Sent at <b>%X</b> on <b>%A</b>");
-
-        } else if (date.getYear() == now.getYear()) {
-            /* Translators: this is a time format in the style of "Wednesday, May 25",
-             shown when you get a chat message in the same year. */
-            // xgettext:no-c-format
-            format = _("Sent on <b>%A</b>, <b>%B %d</b>");
-        } else {
-            /* Translators: this is a time format in the style of "Wednesday, May 25, 2012",
-             shown when you get a chat message in a different year. */
-            // xgettext:no-c-format
-            format = _("Sent on <b>%A</b>, <b>%B %d</b>, %Y");
-        }
-
-        return date.toLocaleFormat(format);
-    },
-
-    appendTimestamp: function() {
-        let lastMessageTime = this._history[0].time;
-        let lastMessageDate = new Date(lastMessageTime * 1000);
-
-        let timeLabel = this._append({ body: this._formatTimestamp(lastMessageDate),
-                                       group: 'meta',
-                                       styles: ['chat-meta-message'],
-                                       childProps: { expand: true, x_fill: false,
-                                                     x_align: St.Align.END },
-                                       noTimestamp: true,
-                                       timestamp: lastMessageTime });
-
-        this._filterMessages();
-
-        return false;
-    },
-
-    appendAliasChange: function(oldAlias, newAlias) {
-        oldAlias = GLib.markup_escape_text(oldAlias, -1);
-        newAlias = GLib.markup_escape_text(newAlias, -1);
-
-        /* Translators: this is the other person changing their old IM name to their new
-           IM name. */
-        let message = '<i>' + _("%s is now known as %s").format(oldAlias, newAlias) + '</i>';
-
-        let label = this._append({ body: message,
-                                   group: 'meta',
-                                   styles: ['chat-meta-message'] });
-
-        this.update(newAlias, null, { customContent: true });
-
-        this._filterMessages();
-    },
-
-    _onEntryActivated: function() {
-        let text = this._responseEntry.get_text();
-        if (text == '')
-            return;
-
-        this._inputHistory.addItem(text);
-
-        // Telepathy sends out the Sent signal for us.
-        // see Source._messageSent
-        this._responseEntry.set_text('');
-        this.source.respond(text);
-    },
-
-    _composingStopTimeout: function() {
-        this._composingTimeoutId = 0;
-
-        this.source.setChatState(Tp.ChannelChatState.PAUSED);
-
-        return false;
-    },
-
-    _onEntryChanged: function() {
-        let text = this._responseEntry.get_text();
-
-        // If we're typing, we want to send COMPOSING.
-        // If we empty the entry, we want to send ACTIVE.
-        // If we've stopped typing for COMPOSING_STOP_TIMEOUT
-        //    seconds, we want to send PAUSED.
-
-        // Remove composing timeout.
-        if (this._composingTimeoutId > 0) {
-            Mainloop.source_remove(this._composingTimeoutId);
-            this._composingTimeoutId = 0;
-        }
-
-        if (text != '') {
-            this.source.setChatState(Tp.ChannelChatState.COMPOSING);
-
-            this._composingTimeoutId = Mainloop.timeout_add_seconds(
-                COMPOSING_STOP_TIMEOUT,
-                Lang.bind(this, this._composingStopTimeout));
-        } else {
-            this.source.setChatState(Tp.ChannelChatState.ACTIVE);
-        }
-    }
-});
-
-const ApproverSource = new Lang.Class({
-    Name: 'ApproverSource',
-    Extends: MessageTray.Source,
-
-    _init: function(dispatchOp, text, gicon) {
-        this._gicon = gicon;
-
-        this.parent(text);
-
-        this._dispatchOp = dispatchOp;
-
-        // Destroy the source if the channel dispatch operation is invalidated
-        // as we can't approve any more.
-        this._invalidId = dispatchOp.connect('invalidated',
-                                             Lang.bind(this, function(domain, code, msg) {
-            this.destroy();
-        }));
-    },
-
-    destroy: function() {
-        if (this._invalidId != 0) {
-            this._dispatchOp.disconnect(this._invalidId);
-            this._invalidId = 0;
-        }
-
-        this.parent();
-    },
-
-    createIcon: function(size) {
-        return new St.Icon({ gicon: this._gicon,
-                             icon_size: size });
-    }
-});
-
-const RoomInviteNotification = new Lang.Class({
-    Name: 'RoomInviteNotification',
-    Extends: MessageTray.Notification,
-
-    _init: function(source, dispatchOp, channel, inviter) {
-        this.parent(source,
-                    /* translators: argument is a room name like
-                     * room@jabber.org for example. */
-                    _("Invitation to %s").format(channel.get_identifier()),
-                    null,
-                    { customContent: true });
-        this.setResident(true);
-
-        /* translators: first argument is the name of a contact and the second
-         * one the name of a room. "Alice is inviting you to join room@jabber.org
-         * for example. */
-        this.addBody(_("%s is inviting you to join %s").format(inviter.get_alias(), channel.get_identifier()));
-
-        this.addButton('decline', _("Decline"));
-        this.addButton('accept', _("Accept"));
-
-        this.connect('action-invoked', Lang.bind(this, function(self, action) {
-            switch (action) {
-            case 'decline':
-                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
-                                                '', function(src, result) {
-                    src.leave_channels_finish(result)});
-                break;
-            case 'accept':
-                dispatchOp.handle_with_time_async('', global.get_current_time(),
-                                                  function(src, result) {
-                    src.handle_with_time_finish(result)});
-                break;
-            }
-            this.destroy();
-        }));
-    }
-});
-
-// Audio Video
-const AudioVideoNotification = new Lang.Class({
-    Name: 'AudioVideoNotification',
-    Extends: MessageTray.Notification,
-
-    _init: function(source, dispatchOp, channel, contact, isVideo) {
-        let title = '';
-
-        if (isVideo)
-             /* translators: argument is a contact name like Alice for example. */
-            title = _("Video call from %s").format(contact.get_alias());
-        else
-             /* translators: argument is a contact name like Alice for example. */
-            title = _("Call from %s").format(contact.get_alias());
-
-        this.parent(source, title, null, { customContent: true });
-        this.setResident(true);
-
-        this.addButton('reject', _("Reject"));
-        /* translators: this is a button label (verb), not a noun */
-        this.addButton('answer', _("Answer"));
-
-        this.connect('action-invoked', Lang.bind(this, function(self, action) {
-            switch (action) {
-            case 'reject':
-                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
-                                                '', function(src, result) {
-                    src.leave_channels_finish(result)});
-                break;
-            case 'answer':
-                dispatchOp.handle_with_time_async('', global.get_current_time(),
-                                                  function(src, result) {
-                    src.handle_with_time_finish(result)});
-                break;
-            }
-            this.destroy();
-        }));
-    }
-});
-
-// File Transfer
-const FileTransferNotification = new Lang.Class({
-    Name: 'FileTransferNotification',
-    Extends: MessageTray.Notification,
-
-    _init: function(source, dispatchOp, channel, contact) {
-        this.parent(source,
-                    /* To translators: The first parameter is
-                     * the contact's alias and the second one is the
-                     * file name. The string will be something
-                     * like: "Alice is sending you test.ogg"
-                     */
-                    _("%s is sending you %s").format(contact.get_alias(),
-                                                     channel.get_filename()),
-                    null,
-                    { customContent: true });
-        this.setResident(true);
-
-        this.addButton('decline', _("Decline"));
-        this.addButton('accept', _("Accept"));
-
-        this.connect('action-invoked', Lang.bind(this, function(self, action) {
-            switch (action) {
-            case 'decline':
-                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
-                                                '', function(src, result) {
-                    src.leave_channels_finish(result)});
-                break;
-            case 'accept':
-                dispatchOp.handle_with_time_async('', global.get_current_time(),
-                                                  function(src, result) {
-                    src.handle_with_time_finish(result)});
-                break;
-            }
-            this.destroy();
-        }));
-    }
-});
-
-// Subscription request
-const SubscriptionRequestNotification = new Lang.Class({
-    Name: 'SubscriptionRequestNotification',
-    Extends: MessageTray.Notification,
-
-    _init: function(source, contact) {
-        this.parent(source,
-                    /* To translators: The parameter is the contact's alias */
-                    _("%s would like permission to see when you are online").format(contact.get_alias()),
-                    null, { customContent: true });
-
-        this._contact = contact;
-        this._connection = contact.get_connection();
-
-        let layout = new St.BoxLayout({ vertical: false });
-
-        // Display avatar
-        let iconBox = new St.Bin({ style_class: 'avatar-box' });
-        iconBox._size = 48;
-
-        let textureCache = St.TextureCache.get_default();
-        let file = contact.get_avatar_file();
-
-        if (file) {
-            let uri = file.get_uri();
-            iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size);
-        }
-        else {
-            iconBox.child = new St.Icon({ icon_name: 'avatar-default',
-                                          icon_size: iconBox._size });
-        }
-
-        layout.add(iconBox);
-
-        // subscription request message
-        let label = new St.Label({ style_class: 'subscription-message',
-                                   text: contact.get_publish_request() });
-
-        layout.add(label);
-
-        this.addActor(layout);
-
-        this.addButton('decline', _("Decline"));
-        this.addButton('accept', _("Accept"));
-
-        this.connect('action-invoked', Lang.bind(this, function(self, action) {
-            switch (action) {
-            case 'decline':
-                contact.remove_async(function(src, result) {
-                    src.remove_finish(result)});
-                break;
-            case 'accept':
-                // Authorize the contact and request to see his status as well
-                contact.authorize_publication_async(function(src, result) {
-                    src.authorize_publication_finish(result)});
-
-                contact.request_subscription_async('', function(src, result) {
-                    src.request_subscription_finish(result)});
-                break;
-            }
-
-            // rely on _subscriptionStatesChangedCb to destroy the
-            // notification
-        }));
-
-        this._changedId = contact.connect('subscription-states-changed',
-            Lang.bind(this, this._subscriptionStatesChangedCb));
-        this._invalidatedId = this._connection.connect('invalidated',
-            Lang.bind(this, this.destroy));
-    },
-
-    destroy: function() {
-        if (this._changedId != 0) {
-            this._contact.disconnect(this._changedId);
-            this._changedId = 0;
-        }
-
-        if (this._invalidatedId != 0) {
-            this._connection.disconnect(this._invalidatedId);
-            this._invalidatedId = 0;
-        }
-
-        this.parent();
-    },
-
-    _subscriptionStatesChangedCb: function(contact, subscribe, publish, msg) {
-        // Destroy the notification if the subscription request has been
-        // answered
-        if (publish != Tp.SubscriptionState.ASK)
-            this.destroy();
-    }
-});
-
-// Messages from empathy/libempathy/empathy-utils.c
-// create_errors_to_message_hash()
-
-/* Translator note: these should be the same messages that are
- * used in Empathy, so just copy and paste from there. */
-let _connectionErrorMessages = {};
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.NETWORK_ERROR)]
-  = _("Network error");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.AUTHENTICATION_FAILED)]
-  = _("Authentication failed");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_ERROR)]
-  = _("Encryption error");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_PROVIDED)]
-  = _("Certificate not provided");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_UNTRUSTED)]
-  = _("Certificate untrusted");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_EXPIRED)]
-  = _("Certificate expired");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED)]
-  = _("Certificate not activated");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_HOSTNAME_MISMATCH)]
-  = _("Certificate hostname mismatch");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_FINGERPRINT_MISMATCH)]
-  = _("Certificate fingerprint mismatch");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_SELF_SIGNED)]
-  = _("Certificate self-signed");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CANCELLED)]
-  = _("Status is set to offline");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_NOT_AVAILABLE)]
-  = _("Encryption is not available");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INVALID)]
-  = _("Certificate is invalid");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REFUSED)]
-  = _("Connection has been refused");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_FAILED)]
-  = _("Connection can't be established");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_LOST)]
-  = _("Connection has been lost");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ALREADY_CONNECTED)]
-  = _("This account is already connected to the server");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REPLACED)]
-  = _("Connection has been replaced by a new connection using the same resource");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.REGISTRATION_EXISTS)]
-  = _("The account already exists on the server");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.SERVICE_BUSY)]
-  = _("Server is currently too busy to handle the connection");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_REVOKED)]
-  = _("Certificate has been revoked");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INSECURE)]
-  = _("Certificate uses an insecure cipher algorithm or is cryptographically weak");
-_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)]
-  = _("The length of the server certificate, or the depth of the server certificate chain, exceed the limits imposed by the cryptography library");
-_connectionErrorMessages['org.freedesktop.DBus.Error.NoReply']
-  = _("Internal error");
-
-const AccountNotification = new Lang.Class({
-    Name: 'AccountNotification',
-    Extends: MessageTray.Notification,
-
-    _init: function(source, account, connectionError) {
-        this.parent(source,
-                    /* translators: argument is the account name, like
-                     * name@jabber.org for example. */
-                    _("Connection to %s failed").format(account.get_display_name()),
-                    null, { customContent: true });
-
-        this._label = new St.Label();
-        this.addActor(this._label);
-        this._updateMessage(connectionError);
-
-        this._account = account;
-
-        this.addButton('reconnect', _("Reconnect"));
-        this.addButton('edit', _("Edit account"));
-
-        this.connect('action-invoked', Lang.bind(this, function(self, action) {
-            switch (action) {
-            case 'reconnect':
-                // If it fails again, a new notification should pop up with the
-                // new error.
-                account.reconnect_async(null);
-                break;
-            case 'edit':
-                let cmd = '/usr/bin/empathy-accounts'
-                        + ' --select-account=%s'
-                        .format(account.get_path_suffix());
-                let app_info = Gio.app_info_create_from_commandline(cmd, null, 0);
-                app_info.launch([], global.create_app_launch_context());
-                break;
-            }
-            this.destroy();
-        }));
-
-        this._enabledId = account.connect('notify::enabled',
-                                          Lang.bind(this, function() {
-                                              if (!account.is_enabled())
-                                                  this.destroy();
-                                          }));
-
-        this._invalidatedId = account.connect('invalidated',
-                                              Lang.bind(this, this.destroy));
-
-        this._connectionStatusId = account.connect('notify::connection-status',
-            Lang.bind(this, function() {
-                let status = account.connection_status;
-                if (status == Tp.ConnectionStatus.CONNECTED) {
-                    this.destroy();
-                } else if (status == Tp.ConnectionStatus.DISCONNECTED) {
-                    this._updateMessage(account.connection_error);
-                }
-            }));
-    },
-
-    _updateMessage: function(connectionError) {
-        let message;
-        if (connectionError in _connectionErrorMessages) {
-            message = _connectionErrorMessages[connectionError];
-        } else {
-            message = _("Unknown reason");
-        }
-        this._label.set_text(message);
-    },
-
-    destroy: function() {
-        if (this._enabledId != 0) {
-            this._account.disconnect(this._enabledId);
-            this._enabledId = 0;
-        }
-
-        if (this._invalidatedId != 0) {
-            this._account.disconnect(this._invalidatedId);
-            this._invalidatedId = 0;
-        }
-
-        if (this._connectionStatusId != 0) {
-            this._account.disconnect(this._connectionStatusId);
-            this._connectionStatusId = 0;
-        }
-
-        this.parent();
-    }
-});
index e85354a68ae94cada7e891edce9eeadb5fd78600..f1e8bf6d09ae58573da80c6577ea4ed11058c730 100644 (file)
@@ -62,8 +62,9 @@ struct _ShellPolkitAuthenticationAgent {
   PolkitAgentListener parent_instance;
 
   GList *scheduled_requests;
-
   AuthRequest *current_request;
+
+  gpointer handle;
 };
 
 /* Signals */
@@ -93,55 +94,40 @@ static gboolean initiate_authentication_finish (PolkitAgentListener  *listener,
                                                 GAsyncResult         *res,
                                                 GError              **error);
 
-static void
+void
 shell_polkit_authentication_agent_init (ShellPolkitAuthenticationAgent *agent)
 {
-  gpointer handle;
-  PolkitSubject *subject;
-  GError *error;
+}
 
-  subject = NULL;
+void
+shell_polkit_authentication_agent_register (ShellPolkitAuthenticationAgent *agent,
+                                            GError                        **error_out)
+{
+  GError *error = NULL;
+  PolkitSubject *subject;
 
-  error = NULL;
   subject = polkit_unix_session_new_for_process_sync (getpid (),
                                                       NULL, /* GCancellable* */
                                                       &error);
   if (subject == NULL)
     {
-      if (error) /* polkit version 104 and older don't properly set error on failure */
-        {
-          g_warning ("Error getting session for the process we are in: %s (%s %d)",
-                     error->message,
-                     g_quark_to_string (error->domain),
-                     error->code);
-          g_error_free (error);
-        }
-      else
-        {
-          g_warning ("Error getting session for the process we are in");
-        }
+      if (error == NULL) /* polkit version 104 and older don't properly set error on failure */
+        error = g_error_new (POLKIT_ERROR, POLKIT_ERROR_FAILED,
+                             "PolKit failed to properly get our session");
       goto out;
     }
 
-  handle = polkit_agent_listener_register (POLKIT_AGENT_LISTENER (agent),
-                                           POLKIT_AGENT_REGISTER_FLAGS_NONE,
-                                           subject,
-                                           NULL, /* use default object path */
-                                           NULL, /* GCancellable */
-                                           &error);
-  if (handle == NULL)
-    {
-      g_warning ("Error registering polkit authentication agent: %s (%s %d)",
-                 error->message,
-                 g_quark_to_string (error->domain),
-                 error->code);
-      g_error_free (error);
-      goto out;
-    }
-
-  /* We don't need to register so skip saving handle */
+  agent->handle = polkit_agent_listener_register (POLKIT_AGENT_LISTENER (agent),
+                                                  POLKIT_AGENT_REGISTER_FLAGS_NONE,
+                                                  subject,
+                                                  NULL, /* use default object path */
+                                                  NULL, /* GCancellable */
+                                                  &error);
 
  out:
+  if (error != NULL)
+    g_propagate_error (error_out, error);
+
   if (subject != NULL)
     g_object_unref (subject);
 }
@@ -149,12 +135,8 @@ shell_polkit_authentication_agent_init (ShellPolkitAuthenticationAgent *agent)
 static void
 shell_polkit_authentication_agent_finalize (GObject *object)
 {
-  /* ShellPolkitAuthenticationAgent *agent = SHELL_POLKIT_AUTHENTICATION_AGENT (object); */
-
-  /* Specifically left empty since the object stays alive forever - if code
-   *  is reused it would need to free outstanding requests etc.
-   */
-
+  ShellPolkitAuthenticationAgent *agent = SHELL_POLKIT_AUTHENTICATION_AGENT (object);
+  shell_polkit_authentication_agent_unregister (agent);
   G_OBJECT_CLASS (shell_polkit_authentication_agent_parent_class)->finalize (object);
 }
 
@@ -325,6 +307,27 @@ on_request_cancelled (GCancellable *cancellable,
   g_idle_add (handle_cancelled_in_idle, request);
 }
 
+static void
+auth_request_dismiss (AuthRequest *request)
+{
+  auth_request_complete (request, TRUE);
+}
+
+void
+shell_polkit_authentication_agent_unregister (ShellPolkitAuthenticationAgent *agent)
+{
+  if (agent->scheduled_requests != NULL)
+    {
+      g_list_foreach (agent->scheduled_requests, (GFunc)auth_request_dismiss, NULL);
+      agent->scheduled_requests = NULL;
+    }
+  if (agent->current_request != NULL)
+    auth_request_dismiss (agent->current_request);
+
+  polkit_agent_listener_unregister (agent->handle);
+  agent->handle = NULL;
+}
+
 static void maybe_process_next_request (ShellPolkitAuthenticationAgent *agent);
 
 static void
index a8758ae92ff47906a4f6a85a6effa53ee1422b9c..745f0c983a84d58ee792c4f55c592310ff05b351 100644 (file)
@@ -27,6 +27,9 @@ GType                           shell_polkit_authentication_agent_get_type (void
 ShellPolkitAuthenticationAgent *shell_polkit_authentication_agent_new      (void);
 void                            shell_polkit_authentication_agent_complete (ShellPolkitAuthenticationAgent *agent,
                                                                             gboolean                        dismissed);
+void                            shell_polkit_authentication_agent_register (ShellPolkitAuthenticationAgent *agent,
+                                                                            GError                        **error_out);
+void                            shell_polkit_authentication_agent_unregister (ShellPolkitAuthenticationAgent *agent);
 
 G_END_DECLS